class DBConnection {
public:
...
static DBConnection create(); // DBConnection 객체를 반환하는 함수 (편의상 매개변수 생략)
void close(); // DB 연결 닫기. 이때 연결이 실패하면 예외를 던짐
}
사용자가 직접 DBConnection
객체에 대해 close
를 호출해야 하는 코드이다.
그러나 사용자가 close
를 호출하는 것을 잊을 수도 있다.
이를 방지하기 위해 DBConnection
에 대한 자원 관리 클래스(DBConn
)를 만들고, 그 클래스의 소멸자에서 close
를 호출하게 만든다.
class DBConn {
public:
...
~DBConn()
{
db.close();
}
private:
DBConnection db;
};
DBConn
클래스를 정의함으로써 DBConn
객체가 소멸될 때 DBConnection
객체에 대한 close
함수가 자동으로 호출된다.
문제는 만약 close
에서 예외가 발생하면 DBConn
은 이 예외를 소멸자 밖으로 전파할거라는 것이다.
1. close
에서 예외 발생 시 프로그램을 바로 종료한다. (대개 abort
호출)
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
// close 호출이 실패했다는 로그
std::abort();
}
}
객체 소멸 중에 에러가 발생한 후 프로그램을 계속 실행할 수 없는 상황에서 괜찮은 선택이다.
2. close
를 호출한 곳에서 일어난 예외를 무시한다.
DBConn::~DBConn()
{
try { db.close(); }
catch (...) {
// close 호출이 실패했다는 로그
}
}
무엇이 잘못됐는지에 대한 중요한 정보가 묻혀버리기 때문에 좋은 선택은 아니다.
하지만 프로그램이 신뢰성 있게 실행을 지속할 수 있다면, 프로그램 종료나 미정의 동작으로 인해 입는 위험 감수보다는 예외를 무시하는 게 나을 수도 있다.
위에서 언급한 두 선택지 모두 문제가 있으므로, DBConn
인터페이스를 잘 설계해서 문제에 대처할 기회를 사용자가 가질 수 있도록 만들어보자.
class DBConn {
public:
...
void close()
{
db.close();
closed = true;
}
~DBConn()
{
if (!closed)
{
try { db.close(); } // 사용자가 db 연결을 닫지 않았으면 닫기
catch (...) { // db 연결 닫기에 실패하면, 실패를 알린 후
// close 호출이 실패했다는 로그
... // 프로그램 종료 or 예외 무시
}
}
db.close();
}
private:
DBConnection db;
};
DBConn
에서 close
함수를 직접 제공하여 사용자가 db.close
실행 중에 발생하는 예외를 직접 처리할 수 있도록 한다. 이 기회를 사용자가 무시하더라도 DBConn
의 소멸자에서 close
를 호출해 마무리하겠지만, 소멸자에서 호출하는 close
도 실패해 예외가 발생한다면 아까와 동일한 상황이 발생할 것이다.
중요한 것은 앞서 사용자에게 에러를 처리할 수 있는 기회를 줬다는 것이다.
또한 DBConnection
이 닫혔는지 여부를 가지고, 닫히지 않았을 경우 DBConn
의 소멸자에서 닫을 수도 있어 db 연결이 누출되지 않는다.
즉 위의 코드는 close
호출의 책임을 DBConn
의 소멸자가 아닌 DBConn
의 사용자에게 넘기는 코드이다.
예외를 일으키는 소멸자는 프로그램의 불완전 종료나 미정의 동작의 위험을 내포하기 때문이다.
정리
- 소멸자 안에서 호출하는 함수가 예외를 던질 가능성이 있다면, 프로그램을 종료하거나 예외를 무시해야 한다.
- 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 함수는 반드시 보통(소멸자가 아닌) 함수이어야 한다.