C++ 예외 처리 | try/catch/throw "완벽 정리" [에러 처리]
이 글의 핵심
try·catch·throw 기본부터 RAII·예외 안전성·noexcept까지. 예외와 에러 코드 선택, 실무 패턴과 흔한 실수를 한 번에 정리합니다.
기본 예외 처리
#include <iostream>
#include <stdexcept>
using namespace std;
int divide(int a, int b) {
if (b == 0) {
throw runtime_error("0으로 나눌 수 없습니다");
}
return a / b;
}
int main() {
try {
int result = divide(10, 0);
cout << result << endl;
} catch (runtime_error& e) {
cout << "에러: " << e.what() << endl;
}
return 0;
}
표준 예외 클래스
#include <exception>
#include <stdexcept>
// 기본 예외
exception
// 논리 에러
logic_error
├─ invalid_argument
├─ domain_error
├─ length_error
└─ out_of_range
// 런타임 에러
runtime_error
├─ range_error
├─ overflow_error
└─ underflow_error
여러 예외 처리
try {
// 예외 발생 가능한 코드
} catch (out_of_range& e) {
cout << "범위 초과: " << e.what() << endl;
} catch (invalid_argument& e) {
cout << "잘못된 인자: " << e.what() << endl;
} catch (exception& e) {
cout << "기타 에러: " << e.what() << endl;
} catch (...) {
cout << "알 수 없는 에러" << endl;
}
실전 예시
예시 1: 파일 처리 예외
#include <fstream>
#include <iostream>
#include <stdexcept>
using namespace std;
string readFile(const string& filename) {
ifstream file(filename);
if (!file.is_open()) {
throw runtime_error("파일을 열 수 없습니다: " + filename);
}
string content, line;
while (getline(file, line)) {
content += line + "\n";
}
return content;
}
int main() {
try {
string content = readFile("config.txt");
cout << content << endl;
} catch (runtime_error& e) {
cerr << "에러: " << e.what() << endl;
return 1;
}
return 0;
}
설명: 파일 처리 시 예외를 던져서 에러를 명확하게 처리합니다.
예시 2: 커스텀 예외 클래스
#include <iostream>
#include <exception>
#include <string>
using namespace std;
class InvalidAgeException : public exception {
private:
string message;
public:
InvalidAgeException(int age) {
message = "잘못된 나이: " + to_string(age);
}
const char* what() const noexcept override {
return message.c_str();
}
};
class Person {
private:
string name;
int age;
public:
Person(string n, int a) : name(n) {
if (a < 0 || a > 150) {
throw InvalidAgeException(a);
}
age = a;
}
void print() {
cout << name << ", " << age << "살" << endl;
}
};
int main() {
try {
Person p1("Alice", 25);
p1.print();
Person p2("Bob", 200); // 예외 발생
p2.print();
} catch (InvalidAgeException& e) {
cerr << "에러: " << e.what() << endl;
}
return 0;
}
설명: 도메인 특화 예외를 만들어 에러를 더 명확하게 표현합니다.
예시 3: RAII와 예외 안전성
#include <iostream>
#include <memory>
using namespace std;
class Resource {
public:
Resource() { cout << "리소스 할당" << endl; }
~Resource() { cout << "리소스 해제" << endl; }
void use() { cout << "리소스 사용" << endl; }
};
void dangerousFunction() {
throw runtime_error("에러 발생!");
}
int main() {
try {
// ❌ 수동 관리 (예외 시 누수)
// Resource* r = new Resource();
// dangerousFunction();
// delete r; // 실행 안됨!
// ✅ RAII (자동 해제)
unique_ptr<Resource> r = make_unique<Resource>();
r->use();
dangerousFunction();
// 예외 발생해도 자동으로 해제됨
} catch (exception& e) {
cout << "에러: " << e.what() << endl;
}
return 0;
}
설명: 스마트 포인터를 사용하면 예외 발생 시에도 리소스가 안전하게 해제됩니다.
자주 발생하는 문제
문제 1: 예외를 값으로 캐치
증상: 예외 객체가 잘림 (slicing)
원인: 값으로 캐치하면 파생 클래스 정보 손실
해결법:
// ❌ 잘못된 코드
try {
throw runtime_error("에러");
} catch (exception e) { // 값으로 캐치
// 파생 클래스 정보 손실
}
// ✅ 올바른 코드
try {
throw runtime_error("에러");
} catch (exception& e) { // 참조로 캐치
cout << e.what() << endl;
}
// ✅ const 참조 (더 안전)
catch (const exception& e) {
cout << e.what() << endl;
}
문제 2: 소멸자에서 예외 던지기
증상: 프로그램 비정상 종료
원인: 소멸자에서 예외가 발생하면 terminate() 호출
해결법:
// ❌ 위험한 코드
class Resource {
public:
~Resource() {
throw runtime_error("에러"); // 절대 안됨!
}
};
// ✅ 올바른 코드
class Resource {
public:
~Resource() noexcept {
try {
// 위험한 작업
} catch (...) {
// 예외 삼킴
}
}
};
문제 3: 예외 명세 (exception specification)
증상: throw() 사용 시 경고 또는 에러
원인: throw()는 C++11에서 deprecated
해결법:
// ❌ 구식 (deprecated)
void func() throw(int, runtime_error) {
// ...
}
// ✅ noexcept 사용
void func() noexcept { // 예외 안던짐
// ...
}
void func2() noexcept(false) { // 예외 던질 수 있음
// ...
}
try-catch 기본 정리
try: 예외가 날 수 있는 코드 블록.throw:throw표현식의 타입이 첫 번째로 타입이 일치하는catch로 전달됩니다. 파생 클래스를 잡으려면 기본 클래스보다 앞에 둡니다(아니면 slicing·놓침).catch (...): 타입을 모를 때; 보통 마지막에 두고, 로깅 후std::terminate를 피하기 위해 재던지기 여부를 신중히 결정합니다.
try {
mayThrow();
} catch (const std::invalid_argument& e) {
// 구체적 처리
} catch (const std::exception& e) {
// 표준 예외 공통
} catch (...) {
// 알 수 없는 예외
}
참조로 잡기: catch (const std::exception& e)가 값 복사보다 안전하고 효율적입니다.
예외 vs 에러 코드
| 측면 | 예외 | 에러 코드(expected, optional, bool 등) |
|---|---|---|
| 제어 흐름 | 실패가 드물 때 깔끔 | 실패가 흔한 API에 적합 |
| 성능 | 예외 경로는 상대적으로 비용 큼 | 핫 루프에서 예측 가능 |
| C 연동 | C API와 섞기 어려움 | errno·리턴 코드와 잘 맞음 |
| 가시성 | 함수 시그니처만 보면 던짐 여부가 불명확할 수 있음(C++17까지) | 호출부에서 실패 처리 강제하기 쉬움 |
C++23의 **std::expected<T, E>는 “값 또는 에러”를 타입으로 표현해, 예외 없이도 실패를 명시적으로 전달할 수 있습니다. 팀 규칙으로 “예외는 정말 예외적인 실패만”**으로 한정하면 혼선이 줄어듭니다.
RAII와 예외 안전성
RAII: 자원은 생성자에서 잡고 소멸자에서 놓습니다. 예외가 나도 스택 풀기로 소멸자가 호출되므로 누수를 막습니다.
예외 안전성 보장의 전형적인 세 단계는 다음과 같습니다.
- 기본 보장(basic): 예외가 나도 리소스 누수 없이 유효한(일관성이 깨질 수는 있는) 상태로 남습니다.
- 강한 보장(strong): 실패 시 상태가 변하지 않음(트랜잭션·복사-교체 관용구와 연결).
- 끊김 없는 보장(nothrow): 연산이 예외를 던지지 않음(
noexcept로 표현 가능).
실무에서는 vector::push_back이 실패하면 무엇이 보장되는지처럼 표준 컨테이너의 예외 보장을 알아두면 설계가 쉬워집니다. 커스텀 클래스도 복사 대입·이동 대입이 예외를 낼 때 클래스 불변식이 어떻게 되는지 문서화하는 것이 좋습니다.
noexcept 심화
- 의미: “이 함수는 예외를 밖으로 던지지 않는다.” 위반 시
std::terminate로 이어질 수 있습니다. - 이동 연산:
std::vector가 재할당 시 이동을 쓰려면 이동이noexcept인 경우가 많아, 이동 생성자/대입에noexcept를 붙이는 것이 성능에 직결됩니다. - 소멸자: 소멸자는 암시적으로
noexcept입니다. 소멸자에서 예외를 던지면 안 됩니다.
void swap(MyType& a, MyType& b) noexcept {
// 멤버 swap만 호출하고 예외 없음을 보장할 때
}
noexcept(expr) 형태로 조건부로 지정할 수도 있습니다.
실전 패턴
- 생성자 실패: 생성자에서는 반환값이 없으므로 유효하지 않은 상태를 두기 어렵다면 예외(또는 별도 팩토리 +
expected)를 씁니다. - 경계에서만 catch: 라이브러리 내부는 예외를 전파하고, UI·main 근처에서 로깅·복구합니다.
std::nested_exception: 저수준에서 잡은 예외를 감싸 상위로 올릴 때 원인 보존.- 테스트:
REQUIRE_THROWS_AS(Catch2 등)로 예외 타입을 고정합니다. - 신규 코드 가이드: 핫 루프는 에러 코드, I/O·파싱 실패는 예외처럼 팀 컨벤션을 한 가지로 정합니다.
FAQ
Q1: 예외 처리는 느린가요?
A: 예외가 발생하지 않으면 거의 오버헤드가 없습니다. 예외 발생 시에만 느립니다.
Q2: 언제 예외를 사용해야 하나요?
A:
- 복구 불가능한 에러
- 생성자 실패
- 깊은 호출 스택에서 에러 전파
사용하지 말아야 할 때:
- 정상적인 제어 흐름
- 성능이 중요한 루프 내부
Q3: return vs throw?
A:
- return: 정상 종료, 예상된 결과
- throw: 비정상 상황, 에러
Q4: 모든 예외를 잡아야 하나요?
A: 처리할 수 있는 예외만 잡으세요. 처리 못하면 상위로 전파하는 것이 좋습니다.
Q5: noexcept는 왜 사용하나요?
A:
- 컴파일러 최적화 가능
- move 생성자에 필수 (vector 등에서 move 사용)
- 의도 명확화
Q6: 예외 vs 에러 코드?
A:
- 예외: 깔끔한 코드, 에러 전파 쉬움
- 에러 코드: 성능 중요, C 호환 필요
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 예외 처리 | try-catch-throw와 예외 vs 에러 코드, 언제 뭘 쓸지
- [Go 2주 완성 #05] Day 8~9: 예외 처리의 새로운 접근 - try-catch는 잊어라
- C++ Exception Specifications | “예외 명세” 가이드
관련 글
- JavaScript 에러 처리 | try-catch, Error 객체, 커스텀 에러