예외 던지기
예외 상황이 발생했음을 시스템에 알리고, 그에 해당하는 예외 객체를 생성하여 던지는(throw
) 방법에 대해 자세히 살펴보겠습니다.
throw
문은 예외 처리 메커니즘의 시작점이며, 어떤 타입의 객체를 던지느냐에 따라 catch
블록의 동작이 결정됩니다.
throw
문: 예외 발생 알림
throw
문은 현재 프로그램의 실행 흐름이 더 이상 정상적으로 진행될 수 없음을 알리고, 제어를 catch
블록으로 옮기는 역할을 합니다.
throw
키워드 뒤에는 던지고자 하는 예외 객체(Exception Object) 를 명시합니다.
throw 표현식;
표현식
: 어떤 타입의 객체든 될 수 있습니다. 이 객체가catch
블록으로 전달되어 예외 정보로 활용됩니다.
throw
문의 동작
throw
문이 실행되면,표현식
의 결과로 임시 예외 객체가 생성됩니다.- 현재 함수의 실행이 즉시 중단됩니다.
- 현재 함수부터 호출 스택을 역순으로 따라가며, 해당 예외를 처리할 수 있는
catch
블록을 찾습니다. 이 과정에서 각 스택 프레임에 존재하는 지역 객체들의 소멸자가 호출됩니다. (스택 풀기, Stack Unwinding) - 적절한
catch
블록이 발견되면, 해당catch
블록으로 제어가 넘어갑니다. - 만약
main
함수까지 도달할 때까지 어떤catch
블록도 예외를 처리하지 못하면, 프로그램은std::terminate
함수를 호출하며 비정상적으로 종료됩니다.
어떤 것을 throw
할 것인가?
C++에서 throw
할 수 있는 객체의 타입에는 제한이 없습니다.
정수, 문자열 리터럴(const char*
), std::string
객체, 그리고 사용자 정의 클래스의 객체 등 무엇이든 던질 수 있습니다.
1. 기본 타입의 값 던지기 (int, double, const char) 포인터: 간단한 예외 상황이나 디버깅 목적으로 사용할 수 있습니다.
#include <iostream>
void divide(int a, int b) {
if (b == 0) {
throw 100; // int 타입의 에러 코드 던지기
}
std::cout << "Result: " << a / b << std::endl;
}
void processFile(const char* filename) {
if (filename == nullptr || filename[0] == '\0') {
throw "Filename cannot be empty!"; // const char* (C-스타일 문자열) 던지기
}
// 실제 파일 처리 로직...
std::cout << "File '" << filename << "' processed successfully.\n";
}
int main() {
try {
divide(10, 0);
} catch (int e) {
std::cerr << "Caught integer error code: " << e << std::endl;
}
try {
processFile("");
} catch (const char* msg) {
std::cerr << "Caught string error: " << msg << std::endl;
}
return 0;
}
주의: 기본 타입의 값을 던지는 것은 예외에 대한 정보를 충분히 제공하기 어렵고, 계층적인 예외 처리를 어렵게 하므로 일반적으로 권장되지 않습니다.
2. std::string
객체 던지기:
오류 메시지를 담는 데 유용하지만, std::exception
계층 구조에 포함되지 않으므로 다형성을 활용하기 어렵습니다.
#include <iostream>
#include <string>
void connectToServer(const std::string& address) {
if (address.empty()) {
throw std::string("Connection Error: Server address is empty!"); // std::string 객체 던지기
}
// 실제 연결 로직...
std::cout << "Connected to " << address << std::endl;
}
int main() {
try {
connectToServer("");
} catch (const std::string& e) {
std::cerr << "Connection Failed: " << e << std::endl;
}
return 0;
}
3. 표준 예외 클래스 객체 던지기 (std::exception
파생 클래스):
가장 권장되는 방법입니다. C++ 표준 라이브러리는 <stdexcept>
헤더에 다양한 표준 예외 클래스를 제공합니다. 이들은 모두 std::exception
클래스를 상속받습니다.
std::exception
: 모든 표준 예외의 최상위 기반 클래스.what()
멤버 함수를 통해 오류 메시지를 얻을 수 있습니다.std::logic_error
: 프로그램의 논리적 오류 (예:std::invalid_argument
,std::out_of_range
)std::runtime_error
: 프로그램 실행 중 발생할 수 있는 오류 (예:std::overflow_error
,std::underflow_error
,std::range_error
)
#include <iostream>
#include <stdexcept> // std::invalid_argument, std::out_of_range, std::runtime_error
#include <vector>
void process_data(int data) {
if (data < 0) {
throw std::invalid_argument("Data cannot be negative."); // 논리 오류
}
if (data > 100) {
throw std::out_of_range("Data is too large, must be <= 100."); // 범위 오류
}
std::cout << "Data processed: " << data << std::endl;
}
void simulate_hardware_failure() {
bool failure_detected = true; // 실제로는 하드웨어 상태를 검사
if (failure_detected) {
throw std::runtime_error("Hardware malfunction detected!"); // 런타임 오류
}
std::cout << "Hardware operating normally.\n";
}
int main() {
// std::invalid_argument 예외 처리
try {
process_data(-5);
} catch (const std::invalid_argument& e) {
std::cerr << "Caught invalid_argument: " << e.what() << std::endl;
}
// std::out_of_range 예외 처리
try {
process_data(150);
} catch (const std::out_of_range& e) {
std::cerr << "Caught out_of_range: " << e.what() << std::endl;
}
// std::runtime_error 예외 처리
try {
simulate_hardware_failure();
} catch (const std::runtime_error& e) {
std::cerr << "Caught runtime_error: " << e.what() << std::endl;
}
// 일반 std::exception으로 모든 표준 예외를 잡기
try {
// 다시 던져서 테스트
// process_data(-10); // 주석 해제 시 위에서 잡힘
std::vector<int> v(5);
v.at(10) = 1; // std::out_of_range 발생
} catch (const std::exception& e) { // std::out_of_range는 std::exception을 상속하므로 여기서 잡힘
std::cerr << "Caught generic std::exception: " << e.what() << std::endl;
}
return 0;
}
표준 예외 클래스를 사용하면 예외에 대한 의미론적인 분류가 가능하며, 계층 구조를 통해 다형적인 예외 처리가 용이합니다.
4. 사용자 정의 예외 클래스 객체 던지기:
가장 유연하고 강력한 방법입니다. 프로그램 특정 오류에 대한 상세한 정보를 제공하고 싶을 때 사용합니다. 일반적으로 std::exception
을 상속받아 what()
메서드를 오버라이드하여 사용자 정의 오류 메시지를 제공합니다. (다음 장에서 자세히 다룹니다.)
// MyCustomException.h (예시, 다음 장에서 자세히)
#include <stdexcept>
#include <string>
class MyCustomException : public std::runtime_error {
public:
MyCustomException(const std::string& message)
: std::runtime_error(message) {}
};
// 사용 예시
void process_network_packet(int packet_size) {
if (packet_size < 10) {
throw MyCustomException("Packet size is too small.");
}
if (packet_size > 1024) {
throw MyCustomException("Packet size exceeds maximum limit.");
}
std::cout << "Packet processed, size: " << packet_size << std::endl;
}
int main() {
try {
process_network_packet(5);
} catch (const MyCustomException& e) {
std::cerr << "Caught custom exception: " << e.what() << std::endl;
} catch (const std::exception& e) { // MyCustomException도 std::exception을 상속하므로 여기서도 잡힐 수 있음
std::cerr << "Caught general exception: " << e.what() << std::endl;
}
return 0;
}
throw
시 객체 복사 vs 참조
예외를 던질 때, 던져지는 객체는 throw
문에 의해 임시 객체로 복사(또는 이동)되어 예외 처리 메커니즘으로 전달됩니다.
따라서 예외 객체를 const MyException&
와 같이 const
참조로 잡는 것이 일반적으로 권장됩니다.
이는 불필요한 복사 비용을 줄이고, 던져진 객체의 실제 타입이 보존되어 다형성을 활용할 수 있게 합니다.
// 올바른 예시: 값으로 던지고, const 참조로 잡기
throw MyException("Error details."); // 값으로 던짐
// ...
catch (const MyException& e) { // const 참조로 잡음
// 안전하고 효율적
}
// 잘못된 예시: 포인터로 던지고, 포인터로 잡기 (메모리 관리 문제)
// throw new MyException("Error details."); // new로 동적 할당된 객체 던짐
// // ...
// catch (MyException* e) { // 포인터로 잡음
// delete e; // catch 블록에서 명시적으로 delete 해줘야 함 (번거롭고 실수할 가능성 높음)
// }
// C++에서는 예외를 포인터로 던지는 것을 강하게 비권장합니다.
// 예외 객체의 생명 주기는 시스템이 관리해야 합니다.
예외 발생 시 스택 풀기와 RAII
throw
문이 실행되어 예외가 던져지면, catch
블록이 발견될 때까지 함수 호출 스택을 역순으로 따라가면서 스택 프레임들을 해제합니다.
이 과정에서 각 스택 프레임에 있는 지역 객체들의 소멸자가 자동으로 호출됩니다. 이를 스택 풀기(Stack Unwinding) 라고 합니다.
이러한 스택 풀기 메커니즘은 RAII(Resource Acquisition Is Initialization) 패턴과 결합될 때 매우 강력해집니다.
RAII는 자원(메모리, 파일 핸들, 뮤텍스 등)을 객체에 캡슐화하고, 객체의 생성자에서 자원을 획득하며, 소멸자에서 자원을 해제하도록 설계하는 기법입니다.
RAII의 장점
- 예외 안전성: 예외가 발생하여 스택 풀기가 일어나더라도, 스택에 있는 RAII 객체들의 소멸자가 자동으로 호출되어 자원이 안전하게 해제됩니다. 이는 자원 누수(Resource Leak)를 방지하는 핵심적인 방법입니다.
- 코드 간결성: 자원 해제 코드를 명시적으로 작성할 필요가 줄어듭니다.
#include <iostream>
#include <string>
#include <stdexcept>
#include <fstream> // for std::ofstream
// RAII를 적용한 파일 핸들 관리 클래스
class FileHandler {
private:
std::ofstream _file;
std::string _filename;
public:
FileHandler(const std::string& filename) : _filename(filename) {
_file.open(filename);
if (!_file.is_open()) {
throw std::runtime_error("Failed to open file: " + filename);
}
std::cout << "FileHandler: File '" << _filename << "' opened.\n";
}
void write(const std::string& data) {
if (!_file.is_open()) {
throw std::runtime_error("File is not open for writing.");
}
_file << data << std::endl;
std::cout << "FileHandler: Wrote '" << data << "' to file.\n";
}
// 소멸자에서 파일 자동 닫기 (RAII 핵심)
~FileHandler() {
if (_file.is_open()) {
_file.close();
std::cout << "FileHandler: File '" << _filename << "' closed.\n";
}
}
};
void manipulateFile(const std::string& filename, bool throw_error) {
FileHandler handler(filename); // 파일 열기 (생성자)
handler.write("First line.");
if (throw_error) {
throw std::runtime_error("Simulating an error during file manipulation."); // 예외 발생
}
handler.write("Second line (this might not be written).");
std::cout << "File manipulation finished normally.\n";
}
int main() {
std::cout << "--- Scenario 1: No exception ---\n";
try {
manipulateFile("output1.txt", false);
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
std::cout << "Scenario 1 finished.\n";
std::cout << "\n--- Scenario 2: With exception ---\n";
try {
manipulateFile("output2.txt", true);
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}
std::cout << "Scenario 2 finished.\n";
return 0;
}
--- Scenario 1: No exception ---
FileHandler: File 'output1.txt' opened.
FileHandler: Wrote 'First line.' to file.
FileHandler: Wrote 'Second line (this might not be written).' to file.
File manipulation finished normally.
FileHandler: File 'output1.txt' closed.
Scenario 1 finished.
--- Scenario 2: With exception ---
FileHandler: File 'output2.txt' opened.
FileHandler: Wrote 'First line.' to file.
Caught exception: Simulating an error during file manipulation.
FileHandler: File 'output2.txt' closed. // 예외 발생 시에도 소멸자가 호출되어 파일이 닫힘!
Scenario 2 finished.
Scenario 2
에서 manipulateFile
함수 내부에 예외가 발생했음에도 불구하고, FileHandler
객체의 소멸자가 호출되어 파일이 안전하게 닫히는 것을 확인할 수 있습니다.
이것이 RAII와 예외 처리의 중요한 시너지 효과입니다.
std::unique_ptr
, std::shared_ptr
, std::fstream
등 대부분의 표준 라이브러리 클래스들은 RAII 원칙을 따르므로 예외 상황에서도 자원 관리에 대해 크게 걱정할 필요가 없습니다.