1. 예외 처리란 무엇인가?
1.1. 예외의 정의
예외(Exception)는 프로그램 실행 중에 발생하는 예상치 못한 상황을 의미합니다. 예를 들어, 다음과 같은 상황들이 예외로 간주됩니다:
- 잘못된 사용자 입력: 사용자가 숫자를 입력해야 하는데 문자를 입력한 경우
- 파일이 존재하지 않음: 프로그램이 특정 파일을 열려고 할 때 해당 파일이 없는 경우
- 메모리 부족: 동적 메모리 할당이 실패한 경우
- 배열 인덱스 초과: 배열의 범위를 벗어난 인덱스에 접근하려는 경우
1.2. 예외 처리의 필요성
이러한 예외 상황을 적절히 처리하지 않으면 프로그램이 비정상적으로 종료되거나, 심각한 버그로 이어질 수 있습니다. 따라서 예외 처리는 프로그램의 안정성과 신뢰성을 높이는 데 필수적입니다.
2. C++ 예외 처리의 기본: try
, catch
, throw
2.1. 기본 구조
C++에서는 try
, catch
, throw
키워드를 사용하여 예외 처리를 구현합니다. 각 키워드의 역할은 다음과 같습니다:
try
블록: 예외가 발생할 가능성이 있는 코드를 포함합니다.catch
블록:try
블록에서 발생한 예외를 처리합니다.throw
: 예외를 발생시킵니다.
2.2. 기본 예제
#include <iostream>
#include <stdexcept> // std::runtime_error 포함
int main() {
try {
// 이곳에서 오류가 발생할 가능성이 있는 코드를 작성합니다.
throw std::runtime_error("오류가 발생했습니다!"); // 의도적으로 예외 던지기
} catch (const std::runtime_error& e) {
// catch 블록에서 오류를 처리합니다.
std::cout << "예외 메시지: " << e.what() << std::endl;
}
return 0;
}
위 코드에서는 try
블록 안에서 throw
를 사용해 예외를 발생시키고, catch
블록에서 해당 예외를 잡아 처리합니다. 이렇게 하면 프로그램이 비정상적으로 종료되지 않고, 사용자에게 유용한 오류 메시지를 제공할 수 있습니다.
3. 다양한 예외 처리하기
3.1. 여러 종류의 예외 처리
하나의 프로그램에서는 여러 종류의 예외가 발생할 수 있습니다. C++에서는 여러 개의 catch
블록을 사용하여 다양한 예외를 처리할 수 있습니다.
#include <iostream>
#include <stdexcept>
void testFunction(int value) {
if (value == 0) {
throw std::invalid_argument("0으로 나눌 수 없습니다!");
}
}
int main() {
try {
testFunction(0);
} catch (const std::invalid_argument& e) {
std::cout << "잘못된 인자: " << e.what() << std::endl;
} catch (...) { // 모든 유형의 예외 잡기
std::cout << "알려지지 않은 오류!" << std::endl;
}
return 0;
}
위 코드에서는 testFunction
함수에서 0
을 입력받으면 std::invalid_argument
예외를 발생시킵니다. 이 예외는 첫 번째 catch
블록에서 처리됩니다. 만약 다른 종류의 예외가 발생하면 두 번째 catch
블록에서 처리됩니다.
4. 사용자 정의 예외 클래스 만들기
4.1. 사용자 정의 예외 클래스의 필요성
C++에서는 표준 예외 클래스 외에도 사용자 정의 예외 클래스를 만들어 사용할 수 있습니다. 이를 통해 더 세밀하고 구체적인 예외 처리가 가능합니다.
4.2. 예제: 사용자 정의 예외 클래스
#include <iostream>
#include <exception>
// 사용자 정의 예외 클래스
class MyException : public std::exception {
public:
const char* what() const noexcept override {
return "사용자 정의 오류!";
}
};
void checkValue(int value) {
if (value < 0) throw MyException(); // 음수일 경우 예외 발생
}
int main() {
try {
checkValue(-1); // 음수를 전달하여 예외 발생
} catch (const MyException& e) {
std::cout << "잡힌 오류: " << e.what() << '\n';
}
return 0;
}
위 코드에서는 MyException
이라는 사용자 정의 예외 클래스를 만들고, checkValue
함수에서 음수가 입력되면 이 예외를 발생시킵니다. 이를 통해 특정 조건에 맞춰 예외를 처리할 수 있습니다.
5. 실전 예제: 나누기 계산기
5.1. 예외 처리를 활용한 나누기 계산기
예외 처리를 실제로 활용하는 예제로, 간단한 나누기 계산기를 만들어 보겠습니다. 이 프로그램은 사용자로부터 두 숫자를 입력받아 나눗셈을 수행하되, 분모가 0
인 경우 예외를 발생시킵니다.
#include <iostream>
#include <stdexcept>
double divide(int numerator, int denominator) {
if (denominator == 0) {
throw std::invalid_argument("0으로 나눌 수 없습니다.");
}
return static_cast<double>(numerator) / denominator;
}
int main() {
int a = 10;
int b;
std::cout << "나누고 싶은 숫자를 입력하세요(분모): ";
std::cin >> b;
try {
double result = divide(a, b);
std::cout << "결과: " << result << std::endl;
} catch (const std::invalid_argument& e) {
std::cerr << "오류: " << e.what() << std::endl; // 오류 메시지 출력
}
return 0;
}
이 프로그램은 사용자가 분모로 0
을 입력하면 std::invalid_argument
예외를 발생시키고, 이를 catch
블록에서 처리하여 오류 메시지를 출력합니다.
6. 예외 처리의 고급 기법
6.1. 예외 전파 (Exception Propagation)
예외는 함수 호출 스택을 따라 전파될 수 있습니다. 즉, 예외가 발생한 함수에서 예외를 처리하지 않으면, 해당 예외는 호출한 함수로 전파됩니다. 이는 프로그램의 상위 레벨에서 예외를 처리할 수 있도록 해줍니다.
#include <iostream>
#include <stdexcept>
void functionA() {
throw std::runtime_error("Function A에서 예외 발생!");
}
void functionB() {
functionA();
}
int main() {
try {
functionB();
} catch (const std::runtime_error& e) {
std::cout << "잡힌 예외: " << e.what() << std::endl;
}
return 0;
}
위 코드에서는 functionA
에서 예외가 발생했지만, functionB
와 main
함수로 전파되어 최종적으로 main
함수에서 예외를 처리합니다.
6.2. 스택 풀기 (Stack Unwinding)
예외가 발생하면, C++는 스택을 풀어가며 예외를 처리할 수 있는 catch
블록을 찾습니다. 이 과정에서 지역 변수들은 소멸자가 호출되어 자원이 정리됩니다.
#include <iostream>
#include <stdexcept>
class Resource {
public:
Resource() { std::cout << "Resource 할당" << std::endl; }
~Resource() { std::cout << "Resource 해제" << std::endl; }
};
void function() {
Resource res;
throw std::runtime_error("예외 발생!");
}
int main() {
try {
function();
} catch (const std::runtime_error& e) {
std::cout << "잡힌 예외: " << e.what() << std::endl;
}
return 0;
}
위 코드에서는 function
에서 예외가 발생하면, Resource
객체의 소멸자가 호출되어 자원이 정리됩니다.
7. 실제 프로젝트에서의 예외 처리 활용
7.1. 파일 입출력에서의 예외 처리
실제 프로젝트에서는 예외 처리를 통해 다양한 오류 상황을 관리할 수 있습니다. 예를 들어, 파일 입출력, 네트워크 통신, 데이터베이스 접근 등에서 발생할 수 있는 오류를 예외 처리로 관리할 수 있습니다.
#include <iostream>
#include <fstream>
#include <stdexcept>
void readFile(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
throw std::runtime_error("파일을 열 수 없습니다: " + filename);
}
std::string line;
while (std::getline(file, line)) {
std::cout << line << std::endl;
}
file.close();
}
int main() {
try {
readFile("nonexistent_file.txt");
} catch (const std::runtime_error& e) {
std::cerr << "오류: " << e.what() << std::endl;
}
return 0;
}
위 코드에서는 파일을 열 수 없는 경우 예외를 발생시켜 오류를 처리합니다.
8. 예외 처리의 장단점
8.1. 장점
- 오류 처리의 일관성: 예외 처리를 통해 오류를 일관된 방식으로 처리할 수 있습니다.
- 코드 가독성: 오류 처리 코드를 비즈니스 로직과 분리할 수 있어 코드의 가독성이 높아집니다.
- 자원 관리: 스택 풀기를 통해 자원이 자동으로 정리됩니다.
8.2. 단점
- 성능 오버헤드: 예외 처리는 일반적인 코드 실행보다 더 많은 오버헤드를 발생시킬 수 있습니다.
- 복잡성: 예외 처리를 잘못 사용하면 코드가 복잡해질 수 있습니다.
9. 예외 처리의 모범 사례
9.1. 예외는 진짜 예외 상황에만 사용하라
예외 처리는 예상치 못한 상황에 사용해야 합니다. 예를 들어, 사용자 입력이 잘못된 경우와 같은 예상 가능한 오류는 예외 처리보다는 조건문을 사용하여 처리하는 것이 더 적절할 수 있습니다.
9.2. 예외 메시지는 명확하게 작성하라
예외 메시지는 오류의 원인을 명확히 알 수 있도록 작성해야 합니다. 이를 통해 디버깅이 용이해집니다.
9.3. 자원 관리는 RAII를 활용하라
RAII(Resource Acquisition Is Initialization)는 C++에서 자원 관리를 위한 중요한 기법입니다. RAII를 활용하면 예외가 발생하더라도 자원이 안전하게 정리됩니다.
10. 결론
예외 처리는 C++ 프로그래밍에서 매우 중요한 개념입니다. try
, catch
, throw
를 적절히 활용하면 프로그램의 안정성을 크게 높일 수 있습니다. 또한, 사용자 정의 예외 클래스를 만들어 더 세밀한 예외 처리를 할 수 있습니다. 예외 처리를 잘 활용하면 프로그램이 예상치 못한 상황에서도 안정적으로 동작하도록 만들 수 있습니다.
'프로그래밍 > C++' 카테고리의 다른 글
C++ 프로젝트 실습: 간단한 계산기 만들기부터 디버깅 및 테스트까지 (0) | 2025.02.01 |
---|---|
C++ 고급 프로그래밍: 네임스페이스, 템플릿, 멀티스레딩의 활용 (0) | 2025.02.01 |
C++ STL 컨테이너: 벡터, 리스트, 맵, 셋 활용 가이드 (0) | 2025.02.01 |
C++ 파일 입출력: 파일 읽기와 쓰기 (0) | 2025.02.01 |
C++ 메모리 관리: 동적 메모리 할당과 해제의 중요성 (0) | 2025.02.01 |