- 소멸자에서는 예외가 빠져나가면 안 된다. 만약 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸자에서 모두 처리해야 한다.
- 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 소멸자가 아니어야 한다.
예외가 소멸자를 떠나지 않게 하자
소멸자로부터 예외가 터져 나가는 경우를 C++ 언어에서 막는 것은 아니지만, 실제 상황을 보면 확실히 막아야 하긴 하다.
class Widget {
public:
...
~Widget () { ... } // 이 함수에서 예외가 발생한다고 가정
};
void doSomething()
{
std::vector<Widget> v;
...
} // v 자동 소멸
vector 타입의 객체 v는 자신이 갖고 있는 Widget들 전부를 소멸시킬 책임이 있다.
v에 들어 있는 Widget이 열 개인데, 첫 번째 것을 소멸시키는 도중에 예외가 발생되었다고 가정해 보자.
나머지 아홉 개는 여전히 소멸되어야 하므로 v는 이들에 대해 소멸자를 호출해야 한다.
그런데 두 번째 Widget객체가 소멸자에서 예외가 발생하면 어떻게 될까?
현재 활성화된 예외가 동시에 두 개나 만들어진 상태이고, 이 두 예외가 발생한 조건에 따라 프로그램이 종료되거나 미정의 동작을 보이게 될 텐데 이러한 경우에는 미정의 동작으로 분류된다.
다른 STL 컨테이너나 TR1의 컨테이너도 동일하게 적용된다.
이렇게 된 원인은 컨테이너를 사용해서가 아니라 예외가 소멸자에서 빠져나왔기 때문이다.
다른 예시이다. 데이터베이스 연결을 나타내는 클래스를 쓰고 있다고 가정해 보자.
class DBConnection {
public:
...
static DBConnection create(); // DBConnection 객체를 반환하는 함수
void close(); // 연결 닫기(연결 실패시 예외발생)
};
사용자가 DBConnection 객체에 대해 close를 직접 호출해야 하는 설계이다.
사용자를 배려하는 방법으로는 DBConnection에 대한 자원 관리 클래스를 만들어서 그 클래스의 소멸자에서 close를 호출하게 만드는 것이다. (RAII)
class DBConn {
public:
...
~DBConn()
{
db.close(); // DBConnection close
}
private:
DBConnection db;
};
// 사용자
{
DBConn dbc(DBConnection::create());
...
}
DBConn 객체가 스코프를 벗어나면 소멸자가 호출되며 DBConnection의 close를 호출하는 구조이다.
하지만 close 중 예외가 발생한다면 DBConn의 소멸자는 분명히 이 예외를 전파할 것이다.
즉, 예외가 소멸자 밖으로 빠져나가는 것이다.
이러한 현상을 피하는 방법은 두 가지이다.
- close에서 예외가 발생하면 프로그램을 바로 끝낸다. (abort)
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
std::abort();
}
}
객체 소멸이 진행되다가 에러가 발생한 후 프로그램 실행을 계속할 수 없는 상황이라면 괜찮은 선택이다.
문제가 발생할 가능성이 있는 코드까지 도달하지 못하게 미리 막는 의도이다.
- close를 호출한 곳에서 일어난 예외를 삼켜 버린다.
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
//log
}
}
대부분의 경우에서는 좋은 방법은 아니다. 무엇이 잘못됐는지 알려주는 중요한 정보가 묻혀 버리기 때문이다.
하지만 때에 따라 불완전한 프로그램 종료 혹은 미정의 동작으로 인해 생기는 위험을 감수하는 것보다는 괜찮을 수 있다.
단, 이후에 프로그램이 문제없이 실행을 지속할 수 있어야 한다.
더 나은 설계
완벽한 해결책은 아니지만 유연하게 대처하는 방안은 다음과 같다.
문제가 발생할 수 있는 부분에 대해 대처할 기회를 사용자에게 제공하는 것이다.
만약 사용자가 직접 처리하지 않았다면 소멸자에서 마저 처리하는 방식이다.
class DBConn {
public:
...
void close()
{
db.close();
closed = true;
}
~DBConn()
{
if(!closed)
{
try {
db.close();
}
catch (...) {
//log
...
}
}
}
private:
DBConnection db;
bool closed;
};
인터페이스가 혼잡하고 책임 전가하는 것처럼 보일 수 있다.
하지만, 어떤 동작이 예외를 일으키면서 실패할 가능성이 있고 예외를 처리해야 한다면, 그 예외는 소멸자가 아닌 다른 함수에서 발생해야 한다는 것이 포인트이다. 예외를 일으키는 소멸자는 프로그램의 불완전 종료 혹은 미정의 동작의 위험을 내포하기 때문이다.
정리하자면 예외가 발생할 수 있는 부분은 소멸자가 아닌 다른 함수에서 처리할 수 있게 하고 만약 사용자가 제대로 예외처리를 하지 않는다면 소멸자에서 예외 처리를 끝내야 한다.