08: 예외가 소멸자를 떠나지 못하도록 붙들어 놓자
소멸자에서 예외가 발생한다면 정의되지 않은 동작을 보인다. 완전하지 못한 프로그램 종료나 미정의 동작의 원인은 예외가 터져 나오는 것을 내버려 두는 소멸자에게 있다. C++는 예외를 내보내는 소멸자는 좋아하지 않는다.
class DBConnection
{
public:
static DBConnection Create(); // DBConnection 객체 반환
void close(); // 연결을 닫는다. 이때 실패하면 예외 던짐
};
class DBConn
{
public:
~DBConn() // 데이터베이스 연결이 항상 닫히도록
{ // 항상 챙겨주는 함수
db.close();
}
private:
DBConnection db;
};
{
DBConn dbc(DBConnection::Create());
}
데이터베이스 연결을 나타내는 클래스를 쓰고 있다고 가정하자. 사용자의 망각을 사전에 차단하는 좋은 방법이라면 DBConnection에 대한 자원 관리 클래스를 만들어서 그 클래스의 소멸자에서 close를 호출하게 만드는 것이다. close 호출이 제대로 성공한다면 아무 문제될 것이 없는 코드이다. 하지만 close에서 예외가 발생했다면 DBConn의 소멸자는 반드시 예외를 전파할 것이다. 그 소멸자에서 예외가 나가도록 내버려 둔다는 것이다. 이것이 문제이다. 예외를 던지는 소멸자는 곧 '걱정거리'를 의미하기 때문이다.
걱정거리를 피하는 방법은 두 가지이다.
~DBConn() // 데이터베이스 연결이 항상 닫히도록
{ // 항상 챙겨주는 함수
try { db.close(); }
catch (...) {
close 호출 실패 로그;
std::abort();
}
}
예외가 발생하면 프로그램을 바로 끝낸다. 대개 abort를 호출한다. 객체 소멸이 진행되다가 에러가 발생한 후에 프로그램 실행을 계속할 수 없는 상황이라면 꽤 괜찮은 선택이다.
~DBConn() // 데이터베이스 연결이 항상 닫히도록
{ // 항상 챙겨주는 함수
try { db.close(); }
catch (...) {
close 호출 실패 로그;
}
}
close를 호출한 곳에서 일어난 예외를 삼켜 버린다. 대부분의 경우 예외 삼키기는 그리 좋은 발상이 아니다. 중요한 정보가 묻혀 버리기 때문이다.
더 좋은 전략은 DBConn 인터페이스를 잘 설계해서, 발생할 소지가 있는 문제에 대처할 기회를 사용자가 가질 수 있도록 하는 것이다. 이를테면, DBConnection에서 close 함수를 직접 제공하게 하면 이 함수의 실행 중에 발생하는 예외를 사요자가 직접 처리할 수 있다.
class DBConn
{
public:
void close()
{
db.close();
closed = true;
}
~DBConn()
{
if(!closed) // 사용자가 연결을 안 닫았으면
try { db.close(); } // 여기서 닫아 본다.
catch (...) { // 연결을 닫다가 실패하면, 실패를 알니 후에 예외처리
close 호출 실패 로그;
}
}
private:
DBConnection db;
bool closed;
};
어떤 동작이 예외를 일으키면서 실패할 가능성이 있고 또 그 예외를 처리해야 할 필요가 있다면, 그 예외는 소멸자가 아닌 다른 함수에서 비롯된 것이어야 한다. 예외를 일으키는 소멸자는 시한폭탄이다. 프로그램의 불완전 종료 혹은 미정의 동작의 위험을 내포하고 있다. 위의 코드의 close 함수는 사용자에게 에러를 처리할 수 있는 기회를 주는 것이다. 이것 마저 없다면 사용자는 예외에 대처할 기회를 못 잡게 된다.
이것만은 잊지 말자!
- 소멸자에서는 예외가 빠져나가면 안 된다. 만약 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸자에서 모두 받아낸 후에 삼켜 버리든지 프로그램을 끝내든지 해야 한다.
- 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(즉, 소멸자가 아닌 함수)이어야 한다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 10: 대입 연산자는 *this의 참조자를 반환하게 하자 (0) | 2023.10.06 |
---|---|
[Effective C++] 09: 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자 (1) | 2023.10.05 |
[Effective C++] 07: 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자 (0) | 2023.10.03 |
[Effective C++] 06: 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 (0) | 2023.10.02 |
[Effective C++] 05: C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자 (1) | 2023.10.02 |