로또
8. 예외가 소멸자를 떠나지 못하도록 붙들어 놓자. 본문
다루는 내용
어떤 동작이 예외를 일으키면서 실패할 가능성이 있고, 또 그 예외를 처리해야 할 필요가 있다면, 그 예외은 소멸자가 아닌 다른 함수에서 비롯되어야 한다.
C++은 예외를 내보내는 소멸자를 좋아하지 않는다.
class Widget{
public:
...
~Widget(){...} // 예외가 발생하는 소멸자
}
void DoSomething(){
std::vector<Widget> v;
...
// 소멸자가 호출되며 예외 발생
}
벡터 v에 Widget 변수 10개가 저장되어있다고 가정하자. v가 소멸될 때, 벡터에 속하는 Widget 전부를 소멸시킬 책임은 해당 벡터 변수에 있다.
v[0]의 Widget을 소멸시키는 도중에 예외가 발생했다고 가정하자. 나머지 아홉 개는 여전히 소멸되어야 하므로 변수 v는 이들에 대해 소멸자를 호출해야 할 것이다. 그렇다면 v[1]의 소멸자에서 예외가 또 발생할 수 있다.
아직 처리되지 못하고 동시에 활성화되버린 두 예외가 어떤 조건이냐에 따라 프로그램 실행이 종료되거나 정의되지 않은 동작을 보인다. 이 경우에는 프로그램이 정의되지 않은 동작을 모인다고 한다. 프로그램 종료가 미정의 동작의 원인은 예외가 터져 나오는 것을 내버려 둔 소멸자에 있다. C++은 예외를 내보내는 소멸자를 좋아하지 않는다.
예외의 처리
아래는 DB연결을 위한 DBConnection 클래스와 해당 클래스를 다루는 DBConn 클래스의 예시이다.
class DBConnection{
public:
...
static DBConnection create(); // DB 연결 생성
void close(); // DB 연결 닫기, 실패 시 예외를 던진다.
...
};
// DBConnection 객체를 관리하는 클래스
class DBConn{
public:
...
~DBConn();
private:
DBConnection db;
}
int main(){
DBConn dbconn = DBConnection.create();
...
return 0; // 함수가 끝나며 소멸자에서 db.close() 호출
}
DBConnection 클래스의 close() 함수에서 예외가 발생했다고 가정하고, DB가 연결된 채로 프로그램이 종료되는 것을 방지하기 위해 소멸자에서 close() 함수를 호출한다고 생각해보자.
DBConn의 소멸자는 예외에 대해 다음과 같이 두 가지 선택을 할 수 있다.
1. close에서 예외가 발생하면 프로그램을 바로 끝낸다.
DBConn::~DBConn{
try{
db.close();
}
catch(...){
... // close() 호출이 실패했다는 로그 작성
std::abort();
}
}
2. close를 호출한 곳에서 발생한 예외를 삼켜 버린다.
DBConn::~DBConn{
try{
db.close();
}
catch(...){
... // close() 호출이 실패했다는 로그 작성
}
}
위 두 가지 방법 모두 예외에 대해 프로그램의 종료에 대해서만 관여할 수 있을 뿐, 어떤 예외가 발생할 지 모르므로 예외에 대한 근본적인 처리를 하지 못한다. 대신 사용자에게 예외를 직접 해결할 수 있도록 함수를 별도로 정의하여 기회를 줄 수 있다.
예외가 발생할 수 있는 함수는 별도로 정의하자.
class DBConnection{
public:
...
static DBConnection create();
void close();
...
};
// DBConnection 객체를 관리하는 클래스
class DBConn{
public:
...
~DBConn();
void close();
private:
DBConnection db;
bool closed;
};
DBConn::~DBConn{
if(!closed){
try{
db.close();
}
// 연결을 닫다가 실패하면
catch(...){
... // 호출이 실패했다는 로그 작성
... // 실행을 끝내거나 예외를 삼킨다.
}
}
}
DBConn::close(){
db.close();
closed = true;
}
int main(){
DBConn dbconn = DBConnection.create();
...
return 0;
}
closed라는 변수를 도입하고, DBConn 클래스 내에 close() 함수를 추가하였다.
사용자가 close()를 하였다면, 소멸자에서 close()를 호출하지 않고, 그렇지 않았다면 소멸자에서 close()를 호출하도록 한다.
이러한 방식은 DBConn 클래스를 사용하는 사용자에게 close() 함수를 호출할 수 있게 하여 사용자에게 발생하는 예외를 처리할 수 있는 기회를 제공한다.
이것만은 잊지 말자!
- 소멸자에서는 예외가 빠져나가면 안됩니다. 만약 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸자에서 모두 받아낸 후에 삼켜 버리든지 프로그램을 끝내든지 해야 합니다.
- 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(소멸자가 아닌 함수)이어야 합니다.
'책 > Effective C++' 카테고리의 다른 글
| 10. 대입 연산자는 *this의 참조자를 반환하게 하자. (0) | 2023.11.08 |
|---|---|
| 9. 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자. (0) | 2023.11.07 |
| 7. 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자. (0) | 2023.11.04 |
| 6. 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자. (0) | 2023.11.03 |
| 5. C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자. (1) | 2023.11.02 |