1. 예외 처리란?
예외 처리는 프로그램 실행 중 발생할 수 있는 오류나 예외적인 상황을 관리하는 메커니즘입니다. C++에서는 try
, catch
, throw
키워드를 사용하여 예외 처리를 구현합니다. 예외 처리를 통해 프로그램이 예상치 못한 상황에서도 안정적으로 동작하도록 할 수 있습니다.
1.1 기본 구조
- try 블록: 예외가 발생할 가능성이 있는 코드를 포함합니다.
- throw 문: 특정 조건이 만족되지 않을 때 예외를 발생시킵니다.
- catch 블록:
throw
된 예외를 처리하는 코드가 위치합니다.
1.2 예제: 파일 열기 실패 시 예외 처리
#include <iostream>
#include <fstream>
#include <stdexcept>
void openFile(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
throw std::runtime_error("파일을 열 수 없습니다.");
}
}
int main() {
try {
openFile("없는파일.txt");
} catch (const std::runtime_error& e) {
std::cerr << "오류: " << e.what() << std::endl;
}
return 0;
}
위 코드에서 openFile
함수는 파일을 열려고 시도하고, 실패하면 std::runtime_error
타입의 예외를 던집니다. main
함수에서는 이 예외를 잡아 적절한 오류 메시지를 출력합니다. 이렇게 하면 프로그램이 비정상적으로 종료되지 않고, 사용자에게 오류 메시지를 제공할 수 있습니다.
2. 사용자 정의 예외 클래스
C++에서는 사용자 정의 예외 클래스를 만들어 보다 구체적이고 의미 있는 오류 정보를 제공할 수 있습니다. 기본적으로 모든 사용자 정의 예외 클래스는 std::exception
클래스를 상속받아야 합니다.
2.1 사용자 정의 예외 클래스 예제
#include <iostream>
#include <exception>
class MyException : public std::exception {
public:
const char* what() const noexcept override {
return "내가 만든 사용자 정의 오류입니다.";
}
};
void mightGoWrong() {
bool errorOccurred = true; // 일부 조건에 따라 변경될 수 있음
if (errorOccurred) {
throw MyException();
}
}
int main() {
try {
mightGoWrong();
} catch (const MyException& e) {
std::cerr << "오류: " << e.what() << std::endl;
}
return 0;
}
위 코드에서 MyException
클래스는 what()
함수를 오버라이드하여 자신만의 오류 메시지를 제공합니다. 이렇게 사용자 정의 예외 클래스를 사용하면 더 명확한 정보 전달이 가능합니다. 예를 들어, 특정 도메인에서 발생할 수 있는 오류를 더 세밀하게 표현할 수 있습니다.
2.2 사용자 정의 예외 클래스의 장점
- 오류의 의미를 명확히 전달: 표준 예외 클래스로는 표현하기 어려운 도메인 특화 오류를 정의할 수 있습니다.
- 코드의 가독성 향상: 사용자 정의 예외 클래스를 사용하면 코드의 의도를 더 명확히 표현할 수 있습니다.
- 오류 처리의 유연성: 특정 예외에 대한 처리를 더 세밀하게 제어할 수 있습니다.
3. 예외 안전성
예외 안전성은 프로그램이 예상치 못한 상황에서도 일관된 상태로 유지되는 것을 보장하는 개념입니다. 예외 안전성에는 세 가지 주요 수준이 있습니다.
3.1 예외 안전성의 수준
기본 안전성(Basic Guarantee):
- 함수 호출 후 객체가 유효하며 이전 상태와 관련된 정보가 손실되지 않습니다.
- 하지만 객체들의 내용은 변할 수 있습니다.
강력한 안전성(Strong Guarantee):
- 함수 호출 전후에 객체들이 동일하게 유지되며 모든 변경사항이 롤백됩니다.
- 이는 트랜잭션처럼 작동하여 실패 시 이전 상태로 복구 됩니다.
무조건적 안전성(No-Throw Guarantee):
- 해당 함수 내에서 절대로 예외가 발생하지 않는다는 보장을 제공합니다.
- 주로 생성자나 소멸자와 같은 특별한 경우에 적용됩니다.
3.2 강력한 안전성을 제공하는 코드 예제
#include <iostream>
#include <vector>
#include <stdexcept>
class SafeArray {
public:
SafeArray(size_t size) : data(size), size(size) {}
void set(size_t index, int value) {
if (index >= size)
throw std::out_of_range("Index out of range");
data[index] = value; // 여기서 문제가 생길 경우 전체 배열은 영향을 받지 않아야 함
}
private:
std::vector<int> data;
size_t size;
};
int main() {
try {
SafeArray arr(5);
arr.set(6, 10); // 이 줄에서 out_of_range 에러가 발생함
} catch (const std::exception& e) {
std::cerr << "예외 발생: " << e.what() << '\n';
// 배열은 여전히 유효하며 다른 동작을 계속 진행할 수 있음
}
return 0;
}
위 코드에서는 SafeArray
클래스를 정의하고 있으며, 배열 크기가 초과될 때마다 적절하게 오류를 처리하도록 설계되었습니다. 이 클래스는 기본적인 안전성을 제공하면서도 데이터 무결성을 유지합니다.
4. 예외 처리의 추가적인 고려 사항
4.1 예외 전파
예외는 호출 스택을 따라 전파될 수 있습니다. 즉, 예외가 발생한 함수에서 처리되지 않으면 상위 호출자로 전파됩니다. 이는 예외를 적절한 위치에서 처리할 수 있도록 해줍니다.
void functionA() {
throw std::runtime_error("예외 발생!");
}
void functionB() {
functionA();
}
int main() {
try {
functionB();
} catch (const std::runtime_error& e) {
std::cerr << "예외 처리: " << e.what() << std::endl;
}
return 0;
}
위 코드에서 functionA
에서 발생한 예외는 functionB
를 거쳐 main
함수에서 처리됩니다.
4.2 예외 처리와 리소스 관리
예외가 발생하면 리소스가 누수되지 않도록 주의해야 합니다. 이를 위해 RAII(Resource Acquisition Is Initialization) 패턴을 사용할 수 있습니다. RAII는 리소스의 수명을 객체의 수명에 바인딩하여 리소스 누수를 방지합니다.
#include <iostream>
#include <memory>
class Resource {
public:
Resource() { std::cout << "리소스 획득\n"; }
~Resource() { std::cout << "리소스 해제\n"; }
};
void useResource() {
std::unique_ptr<Resource> res = std::make_unique<Resource>();
throw std::runtime_error("예외 발생!");
}
int main() {
try {
useResource();
} catch (const std::runtime_error& e) {
std::cerr << "예외 처리: " << e.what() << std::endl;
}
return 0;
}
위 코드에서 std::unique_ptr
를 사용하여 리소스를 관리하면 예외가 발생해도 리소스가 자동으로 해제됩니다.
5. 예외 처리의 추가적인 고려 사항
5.1 예외 처리와 성능
예외 처리는 프로그램의 안정성을 높이지만, 성능에 영향을 미칠 수 있습니다. 예외가 발생하면 호출 스택을 거슬러 올라가며 예외를 처리해야 하기 때문에 추가적인 오버헤드가 발생할 수 있습니다. 따라서 예외 처리는 예외적인 상황에만 사용하고, 일반적인 오류 처리는 다른 방법을 고려하는 것이 좋습니다.
5.2 예외 처리와 다중 스레드
다중 스레드 환경에서 예외 처리는 더 복잡해질 수 있습니다. 각 스레드에서 발생한 예외는 해당 스레드 내에서 처리되어야 하며, 다른 스레드로 전파되지 않습니다. 따라서 다중 스레드 프로그램에서는 각 스레드의 예외를 적절히 처리하고, 필요한 경우 스레드 간 통신을 통해 예외 정보를 공유해야 합니다.
#include <iostream>
#include <thread>
#include <stdexcept>
void threadFunction() {
throw std::runtime_error("스레드에서 예외 발생!");
}
int main() {
std::thread t(threadFunction);
try {
t.join();
} catch (const std::exception& e) {
std::cerr << "예외 처리: " << e.what() << std::endl;
}
return 0;
}
위 코드에서는 스레드에서 발생한 예외를 메인 스레드에서 처리합니다. 이를 통해 다중 스레드 환경에서도 예외를 안전하게 처리할 수 있습니다.
6. 결론
C++에서의 예외 처리 및 예외 안전성은 프로그래밍 품질을 높이는 중요한 요소입니다. 잘 설계된 에러 핸들링 시스템은 사용자에게 보다 나은 경험을 제공하고 버그 수정 및 유지보수를 용이하게 합니다. 다양한 수준의 안정성을 이해하고 활용함으로써 더욱 견고한 애플리케이션 개발이 가능합니다.
예외 처리를 통해 프로그램의 안정성을 높이고, 사용자 정의 예외 클래스를 통해 더 명확한 오류 정보를 제공할 수 있습니다. 또한, 예외 안전성을 고려하여 프로그램이 예상치 못한 상황에서도 일관된 상태를 유지할 수 있도록 설계하는 것이 중요합니다. 이러한 기법들을 활용하면 더 안정적이고 유지보수하기 쉬운 코드를 작성할 수 있습니다.
'프로그래밍 > C++' 카테고리의 다른 글
C++ 메모리 관리 및 최적화: 동적 메모리 할당과 객체 수명 관리 (0) | 2025.02.03 |
---|---|
C++ 연산자 오버로딩: 산술 및 관계 연산자 오버로딩의 이해와 활용 (0) | 2025.02.03 |
C++ 네임스페이스와 모듈: 코드 구조화와 관리의 핵심 (0) | 2025.02.03 |
고급 객체 지향 프로그래밍: 다형성, 가상 함수, 추상 클래스 (0) | 2025.02.03 |
C++ 멀티스레딩: 스레드 생성, 뮤텍스, 조건 변수를 활용한 동시성 프로그래밍 (0) | 2025.02.03 |