- 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는 RAII 객체를 사용하자.
- 일반적으로 쓰이는 RAII 클래스는 tr1::shared_ptr, auto_ptr이다.
tr1::shared_ptr이 복사 시의 동작이 직관적이기 때문에 더 좋다. auto_ptr은 원본 객체를 null로 만든다.
자원 관리 객체
자원이란, 사용을 일단 마치고 난 후엔 시스템에 돌려주어야 하는 모든 것을 일컫는다.
특히 동적 할당된 메모리가 있고 파일 서술자, 뮤텍스 잠금, GUI, 폰트, 브러시 등이 있다.
이러한 자원들을 관리하는데 효과적인 방법 중 하나는 자원 관리 객체를 사용하는 것이다.
투자를 모델링해 주는 클래스 라이브러리를 가지고 작업을 한다고 가정하자.
이 라이브러리는 Investment라는 최상위 클래스가 있고, 이것을 기본으로 하여 구체적인 형태의 투자 클래스가 파생된다.
class Investment { ... }; // 최상위 클래스
Investment* createInvestment(); // Investment 클래스 계통 객체 팩토리 함수
이를 통해 얻은 객체를 삭제하는 책임은 호출자에 있다.
void f()
{
Investment *pInv = createInvestment();
...
delete pInv;
}
문제가 없어 보이지만, createInvestment 함수로부터 얻은 투자 객체의 삭제에 실패할 수 있다.
delete가 실행되기 전에 return문을 만나거나 루프 안에 있을 때 continue 혹은 goto 등 루프로부터 빠져나왔을 때가 그런 경우이다. 또한, 예외가 발생하여 delete에 도달하지 못할 가능성도 있다.
물론, 하나하나 따져 가면서 꼼꼼하게 코드를 작성하면 에러를 막을 수 있겠지만, 쉽지는 않은 일이다.
이 현상을 해결하는 방법 중 하나는 자원을 객체에 넣고 그 자원 해제를 소멸자가 담당하도록 만드는 것이다.
스마트 포인터
상당수의 자원이 힙에서 동적으로 할당되고, 하나의 블록 혹은 함수 안에서만 쓰이는 경우가 많기 때문에 스코프를 벗어날 때 자원이 해제되는 것이 맞다. 표준라이브러리에 있는 auto_ptr은 포인터와 비슷하게 동작하는 객체(스마트 포인터)로서, 가리키고 있는 대상에 대해 소멸자가 자동으로 delete를 실행해 주도록 설계되어 있다.
void f()
{
std::auto_ptr<Investment> pInv(createInvesetment());
...
}
간단한 예제이지만, 자원 관리에 객체를 사용하는 방법의 중요한 두 가지 특징을 볼 수 있다.
- 자원을 획득한 후에 자원 관리 객체에게 넘긴다.
createInvestment 함수가 만든 자원은 auto_ptr 객체를 초기화할 때 사용된다. 이는 자원 획득 즉 초기화(RAII)라고 불린다. 자원 획득과 자원 관리 객체의 초기화가 바로 한 문장에서 이루어진다. 획득된 자원으로 자원 관리 객체를 초기화하지 않고 그 자원을 그 객체에 대입하는 경우도 있지만 자원 관리 객체에 넘겨준다는 점은 같다. - 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다.
소멸자는 어떤 객체가 소멸될 때 자동으로 호출되기 때문에 실행 제어를 벗어난다 하더라도 자원 해제가 제대로 이루어지게 된다. 물론 객체를 해제하다가 예외가 발생될 수 있는 상황에 빠지면 문제가 되지만 해결할 수 있는 부분이다.
auto_ptr은 자신이 소멸될 때 자신이 가리키고 있는 대상에 대해 자동으로 delete를 호출하기 때문에 어떤 객체를 가리키는 auto_ptr의 개수가 둘 이상이면 절대로 안된다. 만약 둘 이상이되면 두 번 삭제되어 에러가 발생한다.
이러한 사태를 막기 위해 auto_ptr은 객체를 복사하면 원본 객체는 null로 만든다.
std::auto_ptr<Investment>
pInv1(createInvestment()); // 자원 획득
std::auto_ptr<Investment> pInv2(pInv1); // 복사 시도, pInv1은 null로 변함
pInv1 = pInv2; // 복사 시도, pInv2는 null로 변함
STL 컨터이너 경우엔 원소들이 정성적인 복사 동작을 가져야 하기 때문에, auto_ptr은 이들의 원소로 허용되지 않는다.
auto_ptr을 쓸 수 없는 상황이라면 그 대안으로 참조 카운팅 방식 스마트 포인터(RCSP)가 좋다.
RCSP는 특정한 어떤 자원을 참조하는 외부 객체의 개수를 유지하고 있다가 그 개수가 0이 되면 해당 자원을 자동으로 삭제하는 포인터이다. RCSP의 동작은 가비지 컬렉션과 비슷하지만 순환 참조 상태를 없앨 수 없다는 점이 다르다.
TR1에서 제공하는 tr1::shared_ptr이 대표적인 RCSP이다.
void f()
{
...
std::tr1::shared_ptr<Investment> pInv1(createInvestment()); // 자원 할당
std::tr1::shared_ptr<Investment> pInv2(pInv1); // pInv1, pInv2 모두 같은 객체를 가리킴
...
}
복사 동작이 정상적으로 동작하기 때문에 STL 컨테이너 등의 환경에서도 쓸 수 있다.
auto_ptr, tr1::shared_ptr은 소멸자 내부에서 delete 연산자를 사용한다. delete [] 연산이 아니다.
동적으로 할당한 배열에 대해 auto_ptr, tr1::shared_ptr을 사용하면 안 된다는 뜻이다.
동적으로 할당된 배열은 vector나 string으로 거의 대체할 수있기에 지원하지 않는다.
만약 사용하고 싶다면 부스트를 찾아보면 된다. boost::scoped_array, bost::shared_array등 유용한 것들이 있다.
마지막으로, createInvestment 함수의 반환 타입이 포인터로 되어 있는데, 이 부분에서 문제가 발생할 수 있다.
반환된 포인터에 대한 delete 호출을 호출자 쪽에서 해야 하는데, 그것을 잊어버리고 넘어갈 수 있기 때문이다.
(스마트 포인터를 사용한다고 해도, 반환 값을 스마트 포인터에 저장하는 것을 유지해야 함)
이는 createInvestment의 인터페이스를 수정하면 해결된다. 이는 다음에 자세히 다루겠다.