[ Effective C++ ]
class Widget
{
public:
...
~Widget() { ... } // 이 함수로부터 예외가 발생된다고 가정
};
void doSomething()
{
std::vector<Widget> v;
...
} // v는 여기서 자동으로 소멸
위 예제에서, Widget들을 소멸시킬 책임은 vector에게 있다.
만약 10개의 Widget이 저장되어 있는데, 첫 번째를 소멸시키는 도중에 예외가 발생되었다고 가정하자. 나머지 9개는 memory leak을 막기 위해 반드시 소멸되어야 하므로, 이들에 대해 소멸자를 호출해야 할 것이다.
만약 두 번째 Widget의 소멸자에서도 예외가 던져지면 어떻게 해야 할까? 이 경우에, Undefined-Behavior에 해당된다.
다른 STL 컨테이너, 정적 배열을 써도 마찬가지인데, 근본적인 원인은 바로 예외를 던지도록 내버려두는 소멸자에게 있다.
class DBConnection
{
public:
...
static DBConnection create(); // DBConnection 객체를 반환하는 함수.
// 매개변수는 생략.
void close(); // 연결을 닫음. 이 때 연결이 실패하면
// 예외를 던짐.
};
class DBConn // DBConnection 객체를 관리하는 클래스
{
public:
...
~DBConn() // 데이터베이스 연결이 항상 닫히도록 챙겨주는 소멸자
{
db.close();
}
private:
DBConnection db;
};
위와 같은 데이터베이스 연결을 나타내는 클래스가 있다고 가정하자.
소멸자에서 close()를 호출해주어 연결을 항상 닫아주기를 기대한다.
{
DBConn dbc(DBConnection::create());
...
...
} // DBConn 객체 소멸, DBConnection의 close() 자동 호출
위 사용 예제에서, close()를 호출했는데 여기서 예외가 발생했다고 가정하면 어떻게 될까?
소멸자는 예외를 던질 것이며, 이를 방치할 것이다. 처음 말했던 문제 사항이 생기게 되는 것.
이 경우, 아래와 같은 2 가지 방법을 사용해볼 수 있다.
[1] 프로그램을 바로 종료. 보통 std::abort() 호출.
DBConn::~DBConn() // [1]
{
try { db.close(); }
catch (...)
{
close 호출이 실패했다는 로그 작성;
std::abort();
}
}
더 이상 프로그램 실행을 계속할 수 없는 상황이라면, 나쁘지 않은 선택.
[2] 예외 삼키기.
DBConn::~DBConn() // [2]
{
try { db.close(); }
catch (...)
{
close 호출이 실패했다는 로그 작성;
}
}
무엇이 잘못되어서 예외가 발생했는지 알려주지 않아 좋지 않은 선택이지만, 상황에 따라 프로그램 종료 / 미정의 동작으로 인한 위험을 보는 것 보단 예외를 삼키는 것이 나을 수도 있다.
이 때, 예외를 무시하더라도 반드시 프로그램이 신뢰성있게 실행을 지속할 수 있게 해야 한다.
하지만 2가지 방식 전부 문제점이 존재하는데, 결국 close()가 최초로 예외를 던지게 된 원인에 대해 프로그램이 조치를 취할 수 없다는 것이다.
더 좋은 방식으로, DBConn의 close() 함수를 직접 제공하게 하고, 안전 장치로 소멸자에서 닫혔는지 확인 후, 열려있다면 다시 한 번 close() 호출을 시도하는 것이다.
class DBConn
{
public:
...
void close()
{
db.close();
closed = true;
}
~DBConn()
{
if (!closed) // 사용자가 닫지 않았다면 닫기 시도.
try {
db.close();
}
catch (...) // 실패하면, 종료하거나 예외 삼키기.
{
close 호출이 실패했다는 로그를 작성합니다.
...
}
}
private:
DBConnection db;
bool closed;
};
close() 호출 책임을 소멸자에서 사용자로 떠넘기는 이런 방식은 무책임해 보일 수도 있다.
여기서 핵심은, 어떤 동작이 예외를 일으키면서 실패할 가능성이 있고, 또 그 예외를 처리해야 할 필요가 있다면, 그 예외는 소멸자가 아닌 다른 함수에서 비롯된 것이어야 한다는 것이다.
이유는, 예외를 일으키는 소멸자는 프로그래의 불완전한 종료, 혹은 Undefined-Behavior의 위험을 내포하고 있기 때문이다.
소멸자에서 예외를 던져서 제어할 수 없는 상황이 생기기 보다는, 사용자에게 직접 에러를 처리할 수 있는 기회를 추가적으로 제공하는 것이다.
[ 정리 ]
- 소멸자에서는 예외가 빠져나가면 안 됩니다. 만약 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸자에서 모두 받아낸 후에 삼켜 버리든지, 프로그램을 끝내든지 해야 한다.
- 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(즉, 소멸자가 아닌 "함수")이어야 한다.
'컴퓨터공학 > Effective C++' 카테고리의 다른 글
10. 대입 연산자는 *this의 참조자를 반환하게 하자 (0) | 2025.02.04 |
---|---|
9. 객체 생성 및 소멸 과정 중에는 절대 가상 함수를 호출하지 말자 (0) | 2025.02.03 |
6. 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 (0) | 2025.01.31 |
5. C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자 (0) | 2025.01.31 |
4. 객체를 사용하기 전에 반드시 그 객체를 초기화하자 (0) | 2025.01.19 |