C++/Effective C++

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

KANTAM 2023. 10. 5. 02:29

08: 예외가 소멸자를 떠나지 못하도록 붙들어 놓자

소멸자에서 예외가 발생한다면 정의되지 않은 동작을 보인다. 완전하지 못한 프로그램 종료나 미정의 동작의 원인은 예외가 터져 나오는 것을 내버려 두는 소멸자에게 있다. C++는 예외를 내보내는 소멸자는 좋아하지 않는다.

class DBConnection
{
public:
    static DBConnection Create();           // DBConnection 객체 반환

    void close();                           // 연결을 닫는다. 이때 실패하면 예외 던짐
};

class DBConn
{
public:
    ~DBConn()                               // 데이터베이스 연결이 항상 닫히도록
    {                                       // 항상 챙겨주는 함수
        db.close();     
    }

private:
    DBConnection db;
};

{
    DBConn dbc(DBConnection::Create());
}

데이터베이스 연결을 나타내는 클래스를 쓰고 있다고 가정하자. 사용자의 망각을 사전에 차단하는 좋은 방법이라면 DBConnection에 대한 자원 관리 클래스를 만들어서 그 클래스의 소멸자에서 close를 호출하게 만드는 것이다. close 호출이 제대로 성공한다면 아무 문제될 것이 없는 코드이다. 하지만 close에서 예외가 발생했다면 DBConn의 소멸자는 반드시 예외를 전파할 것이다. 그 소멸자에서 예외가 나가도록 내버려 둔다는 것이다. 이것이 문제이다. 예외를 던지는 소멸자는 곧 '걱정거리'를 의미하기 때문이다.

 

걱정거리를 피하는 방법은 두 가지이다. 

~DBConn()                               // 데이터베이스 연결이 항상 닫히도록
    {                                       // 항상 챙겨주는 함수
        try { db.close(); }
        catch (...) {
            close 호출 실패 로그;
            std::abort();
        }
    }

예외가 발생하면 프로그램을 바로 끝낸다. 대개 abort를 호출한다. 객체 소멸이 진행되다가 에러가 발생한 후에 프로그램 실행을 계속할 수 없는 상황이라면 꽤 괜찮은 선택이다. 

~DBConn()                               // 데이터베이스 연결이 항상 닫히도록
    {                                       // 항상 챙겨주는 함수
        try { db.close(); }
        catch (...) {
            close 호출 실패 로그;
        }
    }

close를 호출한 곳에서 일어난 예외를 삼켜 버린다. 대부분의 경우 예외 삼키기는 그리 좋은 발상이 아니다. 중요한 정보가 묻혀 버리기 때문이다.

 

더 좋은 전략은 DBConn 인터페이스를 잘 설계해서, 발생할 소지가 있는 문제에 대처할 기회를 사용자가 가질 수 있도록 하는 것이다. 이를테면, DBConnection에서 close 함수를 직접 제공하게 하면 이 함수의 실행 중에 발생하는 예외를 사요자가 직접 처리할 수 있다. 

class DBConn
{
public:
    void close()
    {
        db.close();
        closed = true;
    }

    ~DBConn()                               
    {                               
        if(!closed)                 // 사용자가 연결을 안 닫았으면
        try { db.close(); }         // 여기서 닫아 본다.
        catch (...) {               // 연결을 닫다가 실패하면, 실패를 알니 후에 예외처리
            close 호출 실패 로그;
        }
    }

private:
    DBConnection db;
    bool closed;
};

어떤 동작이 예외를 일으키면서 실패할 가능성이 있고 또 그 예외를 처리해야 할 필요가 있다면, 그 예외는 소멸자가 아닌 다른 함수에서 비롯된 것이어야 한다. 예외를 일으키는 소멸자는 시한폭탄이다. 프로그램의 불완전 종료 혹은 미정의 동작의 위험을 내포하고 있다. 위의 코드의 close 함수는 사용자에게 에러를 처리할 수 있는 기회를 주는 것이다. 이것 마저 없다면 사용자는 예외에 대처할 기회를 못 잡게 된다. 

 

이것만은 잊지 말자!

  • 소멸자에서는 예외가 빠져나가면 안 된다. 만약 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸자에서 모두 받아낸 후에 삼켜 버리든지 프로그램을 끝내든지 해야 한다.
  • 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(즉, 소멸자가 아닌 함수)이어야 한다.