항목 8 : 예외가 소멸자를 떠나지 못하도록 붙들어 놓자
소멸자로부터 예외가 터져 나가는 경우를 C++ 언어에서 막는 것은 아니지만, 실제 상황을 들춰보면 확실히 우리가 막을 수 밖에 없는 것 같습니다. 아래의 예를 봅시다.
1
2
3
4
5
6
7
8
|
class DBConnection {
public :
...
static DBConnection create(); // DBConnection 객체를 반환하는 함수. 매개변수는 편의상 생략.
void close(); // 연결을 닫습니다. 이때 연결이 실패하면 예외를 던집니다.
};
Colored by Color Scripter
|
보다시피 사용자가 DBConnection 객체에 대해 close를 직접 호출해야 하는 설계입니다.
자원 관리 클래스의 소멸자가 어떤 형태인지 보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
|
class DBConn { // DBConnection 객체를 관리하는 클래스
public :
...
~DBConn(); // 데이터베이스 연결이 항상 닫히도록 확실히 챙겨주는 함수
{
db.close();
}
private :
DBConnection db;
};
Colored by Color Scripter
|
이렇게 코드를 작성할 시 다음과 같은 프로그래밍이 가능해집니다.
1
2
3
4
5
6
7
8
9
10
|
{ // 블록 시작
DBConn dbc(DBConnection :: create()); // DBConnection 객체를 생성하고 이것을 DBConn 객체로 넘겨서 관리를 맡깁니다.
... // DBConn 인터페이스를 통해 그 DBConnection 객체를 사용합니다.
} // 블록 끝. DBConn 객체가 여기서 소멸됩니다, 따라서 DBConnection 객체에 대한
// close 함수의 호출이 자동으로 이루어집니다.
Colored by Color Scripter
|
close 호출만 정상적으로 동작하면 문제가 될 것이 없는 코드입니다. 그러나 close호출 시 예외가 발생했다고 가정하면 어떻게 될까요?
DBConn의 소멸자는 분명히 이 예외를 전파할 것입니다. 이것이 문제입니다. 예외를 던지는 소멸자는 곧 '걱정거리'를 의미하기 때문입니다.
걱정거리를 피하는 방법은 두 가지 입니다.
첫 번째, close에서 예외가 발생하면 프로그램을 바로 끝냅니다. 대개 abort를 호출합니다.
1
2
3
4
5
6
7
8
|
DBConn :: ~DBConn()
{
try { db.close(); }
catch ( ... ){
close 호출이 실패했다는 로그를 작성;
std :: abort();
}
};
|
두 번째, close를 호출한 곳에서 일어난 예외를 삼켜 버립니다.
1
2
3
4
5
6
7
8
|
DBConn :: ~DBConn()
{
try { db.close(); }
catch ( ... ){
close 호출이 실패했다는 로그를 작성;
std :: abort();
}
};
|
대부분의 경우에서 예외 삼키기는 그리 좋은 발상이 아닙니다. 중요한 정보가 묻혀 버리기 때문입니다.
하지만 때에 따라서는 불완전한 프로그램 종료 혹은 미정의 동작으로 인해 입는 위험을 감수하는 것보다 그냥 예외를 먹어버리는 게 나을 수도 있습니다.
단, 발생한 예외를 그냥 무시한 뒤라도 프로그램이 신뢰성 있게 실행을 지속할 수 있어야 합니다.
이것보다 더 좋은 방법을 생각해 볼 수 있을것 같습니다.
인터페이스를 잘 설계해서, 발생할 소지가 있는 문제에 대처할 기회를 사용자가 가질 수 있도록 하면 어떨까요?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class DBConn {
public :
...
void close() // 사용자 호출을 배려하여 새로 만든 함수
{
db.close();
closed = true;
}
~DBConn()
{
if(!closed)
try{ // 사용자가 연결은 안 닫았으면
db.close(); // 여기서 닫아 봅니다.
}
catch( ... ){ // 연결을 닫다가 실패하면,
close 호출이 실패했다는 로그를 작성합니다; // 실패를 알린 후에
... // 실행을 끝내거나 예외를 삼킵니다.
}
}
private :
DBConnection db;
bool closed;
};
Colored by Color Scripter
|
close 호출의 책임을 DBConn의 소멸자에서 DBConn의 사용자로 떠넘기는 이런 아이디어는 무책임한 책임 전가로 보일 수도 있습니다. 여기서 우리가 알아야 할 것은,
어떤 동작이 예외를 일으키면서 실패할 가능성이 있고 또 그 예외를 처리해야 할 필요가 있다면, 그 예외는 소멸자가 아닌 다른 함수에서 비롯된 것이어야 한다 라는 점 입니다.
꼭 잊지 말아야 할 것!
1. 소멸자에서는 예외가 빠져나가면 안 됩니다. 만약 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸자에서 모두 받아낸 후에 삼켜 버리든지 프로그램을 끝내든지 해야 합니다.
2. 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(즉, 소멸자가 아닌 함수)이어야 합니다.
'언어 > C++' 카테고리의 다른 글
[Effective C++] 대입 연산자는 *this의 참조자를 반환하게 하자 (0) | 2020.01.10 |
---|---|
[Effective C++] 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자 (0) | 2020.01.09 |
[Effective C++] 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자 (0) | 2020.01.07 |
[Effective C++] 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 (0) | 2020.01.06 |
[Effective C++] C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자 (0) | 2020.01.05 |