본문 바로가기

Library/C/C++

예외를 전파하기 전에, 할당한 자원은 반드시 해당 개체에서 회수하라

에러에 대한 대책으로, 예외(exception)가 가지는 장점은 뚜렷하다. 기존의 함수 리턴값에 의존하는 구조로는 다루기 힘든 에러 처리도 예외를 사용하면 훨씬 구조적으로 처리할 수 있는 경우가 많다. 그러나, 제대로 된 예외 처리는 쉽지 않다. 예외에 신경쓰기 시작하면, 대체 어느 것이 예외에 안전한 코드인지, 그리고 예외를 처리하는 와중에도 발생 가능한 예외 때문에 상당히 골머리를 썩이게 된다. 기본 자료형 사이에서의 단순 연산은 예외를 일으키지 않지만, 문제는 사용자에 의해 생성된 자료형이다. 이들은 기본 자료형만으로 구성되어 있지 않기 때문에, 이런 자료형을 사용하는 도중 발생하는 예외는 프로그램 자료구조 일관성에 큰 영향을 미친다.

극단적으로 단순화해서 이야기한다면 힙에 관련된 연산은 언제나 예외 발생 가능성을 염두에 두고 있어야 한다. 개발 당시의 짧은 가동 시간에서는 나타나지 않았던 메모리 고갈 현상이 실제 사용 도중 발생할 수 있으며, 이것을 감안하지 않은 코드는 연쇄적으로 부서지며 디버깅을 미궁 속으로 밀어넣게 된다. 특히, 특정 부분에서 한번에 예외를 잡아내서 일괄처리식으로 자원을 관리하는 것은 매우 좋지 않다. 이와 같은 방식은 반드시 프로그래머가 감지하지 못하는 에러에 의해 놓치는 자원이 발생하기 때문이다. 예외를 사용한다면, 다음 2가지 원칙을 반드시 지키는게 좋다.


1. 자원은 개체에 의해 관리하라 : RAII(Resource Acquisition Is Initialization)이라고 알려져 있는 이 기법은, 언어적으로 보장되는 생성자 - 소멸자 호출 메커니즘을 사용하여 기계적으로 자원을 관리한다. 스마트 포인터의 중요한 목적 중 하나는 자원 관리를 지능적으로 대행하는 것이다.

2. 자원 그 자체를 개체에 의해서 관리하지 않고 생성자 - 소멸자에서 명시적으로 관리한다면, 자원 할당 도중 예외가 발생했을 때 해당 개체는 반드시 예외 발생 시점까지 할당했던 모든 자원을 회수하고 예외를 전파해야 한다.


특히, 2번은 일반적인 의미에서 매우 중요하다. 기본 자료형은 개체가 생성되는 시점에서 완전히 생성되는 것을 보장하기 때문에 이들에 대해서는 신경 쓸 필요가 없지만, 사용자에 의해 생성된 자료형은 이것을 보장하지 않기 때문이다(그래서 이와 같은 데이터는 개체에 의해 관리하는 것이 좋다). C++는 생성자 호출이 정상적으로 완료되지 않았다면 소멸자를 절대로 호출하지 않기 때문에, 사용자 자료형이 개체에 의해 관리되지 않는다면 해당 개체가 호출되기 전의 상태로, 모든 자원을 완전히 회수한 다음 예외를 전파해야 한다. 예외를 발생시킨 개체 외부에서는 개체의 자료구조 일관성을 파악할만한 수단이 거의 없기 때문이다.


최소한 이 두 가지 원칙을 염두에 두고 코드를 작성한다면, 예외 처리 때문에 골머리를 썩힐 일을 크게 줄일 수 있을 것이다.