언어/C++

[Effective C++] 예외가 소멸자를 떠나지 못하도록 붙들어 놓자

지나가던 개발자 2020. 1. 8. 17:18
반응형

항목 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. 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(즉, 소멸자가 아닌 함수)이어야 합니다.

반응형