KoreanFoodie's Study

Effective C++ | 항목 8 : 예외가 소멸자를 떠나지 못하도록 붙들어 놓자 본문

Tutorials/C++ : Advanced

Effective C++ | 항목 8 : 예외가 소멸자를 떠나지 못하도록 붙들어 놓자

GoldGiver 2022. 10. 25. 16:05

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

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

핵심 :

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


다음과 같은 예시를 보자.

class Widget
{
  public:
    ...
    // 이 함수로부터 예외가 발생된다고 가정하자
    ~Widget() { ... }
};

void doSomething()
{
  std::vector<Widget> v;
} // v 는 여기서 자동으로 소멸

벡터 v 가 소멸될 때, 자신이 거느리고 있는 Widget 들 전부를 소멸시킬 책임은 v 에게 있다. 벡터의 길이가 10 인데, 첫 번째 요소를 소멸시키는 와중에 예외가 발생했다고 가정해 보자. 나머지 아홉 개는 여전히 소멸되어야 하므로 v 는 이들에 대해 소멸자를 호출해야 한다. 그런데 이 과정에서 문제가 또 터지면 어떻게 될까?
이 경우에는, 정의되지 않은 동작이 보이게 된다. C++ 는 예외를 내보내는 소멸자를 좋아하지 않는다!

또다른 같은 예시를 보자.

// 데이터베이스 연결을 나타내는 클래스
class DBConnection
{
  public:
    ...
    // DBConnection 객체를 반환한다.
    static DBConnection create();
  
    // 연결을 닫는다. 연결이 실패하면 예외를 던진다.
    void close();
};

// DBConnection 객체를 관리하는 클래스
class DBConn
{
  public:
    ...
    // 데이터베이스 연결이 항상 닫히도록
    // 확실히 챙겨주는 함수
    ~DBConn() { db.close(); }
  private:
    DBConnection db;
};

DBConn 이라는 자원관리 클래스를 사용해 보자.

// 블록 시작
{
  // DBConnection 객체를 생성하고 
  // 이것을 DBConn 객체로 넘겨서 관리를 맡긴다
  DBConn dbc(DBConnection::create());
  
  // DBConn 인터페이스를 통해
  // DBConnection 객체를 사용한다
  ...
  
  // 블록 끝
  // 객체가 소멸된다
  // DBConn 의 소멸자가 호출된다
  // db.Close() 가 호출된다는 뜻이기도 하다
}

위 경우에서, 만약 close 를 호출했는데 여기서 예외가 발생하면 어떻게 될까? 소멸자는 둘 중 하나를 선택해서 이런 '걱정거리'를 회피할 수 있다.

1. close 에서 예외가 발생하면 프로그램을 바로 끝낸다. 대개 abort 를 호출한다.

DBConn::~DBConn()
{
 try { db.close(); }
 catch (...) { std::abort; }
}

미정의 동작을 막는다는 점에선 나쁘지 않은 선택지이다.
2. close 를 호출한 곳에서 일어난 예외를 삼켜 버린다.

DBConn::~DBConn()
{
 try { db.close(); }
 catch (...) { /* 호출 실패 로그 */ }
}

대부분의 경우, 예외 삼키기는 좋은 발상이 아니다. 무엇이 잘못되었는지 알려주는 정보가 묻히기 때문이다. 때에 따라선 이런 방식이 필요할 수도 있는데, 대신 예외를 삼킨 이후, 프로그램이 제대로 동작한다는 보장이 있어야 한다.

위의 두 전략으로 실패 처리를 하기 이전에, close 호출의 책임을 DBConn 의 소멸자에서 DBConn 의 사용자로 떠넘기는 (DBConn 의 소멸자엔 '확인사살' 코드를 두고) 방식을 적용해 보자.

class DBConn
{
  public:
  ...
  
  // 사용자 호출을 배려(?) 해서
  // 새로 만든 함수
  void close()
  {
    db.close();
    closed = true;
  }

  
  ~DBConn()
  {
    if (!closed)
    // 사용자가 연결을 안 닫았으면 여기서 닫아본다
    try { db.close(); }
    // 실패하면 실행을 끝내거나 예외를 삼킨다
    catch (...) { /* 호출 실패 로그 */ }
  }

  private:
    DBConnection db;
    bool closed;
};

포인트는, 예외가 처리되어야 한다면, 그 예외는 소멸자가 아닌 다른 함수로부터 비롯된 것이어야 한다는 것이 포인트이다!

Comments