상세 컨텐츠

본문 제목

항목 8) 소멸자에서의 예외 발생

C++/Effective C++

by DeaKyungLee 2021. 12. 5. 12:09

본문

예외가 소멸자를 떠나지 못하도록 잡아두자.

 

소멸자에서 예외가 발생하는 경우에 대해서 생각해보자.

class Widget {
public:
	// ...
	~Widget() 
	{
		//... 예외 발생!
	}
};
 
void doSomething()
{
	vector<Widget> v;
	// ...
} // 함수 내부이므로 v는 여기서 자동으로 소멸한다.

만약에 v의 첫 번째 요소가 소멸 될 때, 예외가 발생되었다면?

소멸자 내부에서 따로 예외 처리를 하지 않으므로, 예외는 소멸자 외부로 나가게 된다.

그렇더라도 나머지 9개 요소 역시 소멸해야하므로 나머지 소멸자들도 호출된다.

이렇게 두 개 이상의 예외가 동시에 발생하면, 프로그램의 미정의 동작을 하게 된다.

이러한 현상은 stl::vector 뿐만 아니라 모든 자료 구조에서 마찬가지이다.

즉, 소멸자에서 발생한 예외는 반드시 소멸자 내부에서 처리해야 한다.

아래의 예시를 보자.

class DBConnection 
{
public:
	...
	static DBConnection create();	//DBConnection 객체를 반환하는 함수. 매개변수는 편의상 생략한다.
 
	void close();	//연결을 닫는다. 이 때 연결이 실패하면 예외를 던진다.
};

// ...
 
class DBConn 		//DBConnection객체를 관리하는 클래스
{					
private:
	DBConnection db;
 
public:
	...
	~DBConn()			//데이터베이스 연결이 항상 닫히도록 소멸자에서 close 함수를 호출한다.
	{				
		db.close();
	}
};

위의 코드에서 DBConn을 활용하면 사용자가 따로 close 함수를 호출하지 않아도 DBConn 객체가 소멸하면서 close 함수가 자동으로 호출된다.

그런데 만약 close 함수 호출 도중에 예외가 발생하면 어떻게 될까?

앞서 언급했던 예외가 소멸자 밖으로 빠져나가는 최악의 상황이 발생한다.

이러한 현상을 핸들링 하는 방법은 크게 두 가지 종류가 있다.

1. 프로그램을 곧바로 종료시켜 버린다.

DBConn::~DBConn() 
{
	try { db.close(); }
	catch (...) 
	{
		close 호출이 실패했다는 로그 작성;
		std::abort();
	}
}

2. 예외를 무시해버린다.

DBConn::~DBConn() 
{
	try { db.close(); }
	catch (...) 
	{
		close 호출이 실패했다는 로그 작성;
	}
}

짐작하다시피, 2번째 방법은 그다지 좋은 방법이 아니지만 경우에 따라서는 이러한 방식이 나을 수도 있다.

사실 따지고 보면 위의 두 방법 모두 좋은 방법은 아니다.

근본적으로 생각해보면 "close 함수에 대한 호출로 인해서 최초로 발생하는 예외가 어디에서 처리될 것인가?"

이 질문이 핵심이다.

그리고 더 말하자면 당연히 소멸자에서 처리하는 것보다 일반 함수에서 처리하는 것이 훨씬 안전하다.

( 소멸자에서 예외가 빠져나갈 가능성 자체를 만들지 않으므로 )

즉, 소멸자에서 자동으로 close 함수를 호출하는 것 외에도 사용자 프로그래머가 직접 close 함수를 호출할 수 있게 하는 것이 가장 좋은 방법이다.

-> 사용자에게 선택권을 주자는 의미이다.

class DBConn 
{
private:
	DBConnection db;
	bool closed;
 
public:
	...;
	void close()			//사용자가 직접 DB를 닫을 수 있는 함수
	{						
		db.close();
		closed = true;
	}
 
	~DBConn()
	{
		if(!closed)			//사용자가 연결을 안 닫았으면
			try				//여기서 닫는다.
		{
			db.close();
		}
		catch (...)			
		{												//연결을 닫다가 실패하면,
			close 호출이 실패했다는 로그를 작성한다.;	// 실패를 알린 후에 실행을 끝내거나 예외를 삼킨다.
			....
		}
 
	}
 
};

위와 같이 설계하면, 사용자가 직접 DBConn 객체의 close 함수를 통해 DB 닫기 행위를 시도할 수 있다.

물론 DB를 닫는 것이 실패하여서 예외가 발생하는 것 자체는 이전과 다름이 없지만,

해당 예외를 소멸자가 아닌 다른 일반 함수에서 다룰 수 있게 된 것이다.

( 사용자가 DB 닫는 것을 따로 하지 않는다면, 이전과 마찬가지로 소멸자에서 DB 닫기를 시도한다.
즉, 이것은 사용자에게 기회를 주는 의미가 된다. )

더보기
생성자에서의 예외 처리

이것 역시 소멸자와 일맥상통하는 부분이다.
가장 핵심적인 내용은 소멸자, 생성자 모두 리턴값이 존재하지 않는 함수라는 것이다.
따라서 해당 함수에서 실패 처리 관련 코드가 따로 필요한 코드는 지양해야한다. ( ex. 파일 열기, DB Open )
일반 함수라면 리턴값을 통해서 사용자가 해당 실패 처리를 대응할 수 있다.
하지만 생성자와 소멸자라면?
-> 매번 MessageBox 같은 디버그 로그를 찍기?
-> 전역 Flag 변수 값의 변경?
어떤 방식을 사용하더라고 근본적인 해결책은 되지 않는다.
심지어 해당 상황 때문에 별도의 전역 Flag를 따로 두는 행위는 당장 급하다고 사채를 끌어쓰는 것과 마찬가지다.

 

 

Things to Remember

  • 소멸자에서 예외가 전파되어서 빠져나가면 안 된다. 소멸자에서 호출되는 함수가 예외를 발생시킬 가능성이 있다면 반드시 소멸자 자체 내부에서 해당 예외를 받아내서 처리해야 한다.
  • 더 좋은 방법은 소멸자에서 해당 예외가 발생하기 전에 다른 일반 함수에서 처리하는 것이 좋다. 때문에 항상 소멸자에서 예외 발생 가능성이 있는 함수를 호출하는 행위는 다른 일반 함수로도 제공이 되어야한다.

관련글 더보기

댓글 영역