13: 자원 관리에는 객체가 그만!
프로그래밍 분야에서 자원이란, 사용을 일단 마치고 난 후엔 시스템에 돌려주어야 하는 모든 것이다.
class Investment {};
Investment* createInvestment(); // Investment객체를 동적할당하고 그 포인터 반환
// 객체의 해제는 호출자 쪽에서 직접 해
void f()
{
Investment* *pInv = createInvestment(); // 팩토리 함수 호출
... // pInv 사용
delete pInv; // 객체 해제
}
위의 코드는 멀쩡해 보이지만, 객체의 삭제에 실패할 수 있는 경우가 많다.
- delete에 도달 하기 전에 '...' 부분에서 return 문이 들어 있을 수 있다.
- createInvestment 호출문과 delete가 하나의 루프 안에 들어 있고 continue 혹은 goto 문에 의해 갑작스레 루프로부터 빠져나올 수 있다.
- '...' 안의 어떤 문장에서 예외를 던질 수 있다. 예외가 던져지면 delete가 호출되지 않기 때문이다.
모든 경우에서 결과는 같다. 바로 Investment의 객체를 담고 있는 메모리가 누출되고, 그와 동시에 Investment가 갖고 있던 자원까지 모두 샌다.
auto_ptr
얻어낸 자원을 항상 해제되도록 만드는 방법은, 자원을 객체에 넣고 그 자원 해제를 소멸자가 맡도록 하며, 그 소멸자는 실행 제어가 f를 떠날 때 호출되도록 만드는 것이다. 자원을 객체에 넣음으로써, C++가 자동으로 호출해 주는 소멸자에 의해 해당 자원을 저절로 해제할 수 있다.
auto_ptr은 포인터와 비슷하게 동작하는 객체[스마트 포인터(smart pointer)]로서, 가리키고 있는 대상에 대해 소멸자가 자동으로 delete를 불러주도록 설계되어 있다.
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
... // pInv 사용
} // auto_ptr의 소멸자를 통해
// pInv를 삭제
여기서 자원 관리에 객체를 사용하는 방법의 중요한 두 가지 특징을 볼 수 있다.
- 자원을 획득한 후에 자원 관리 객체에게 넘긴다.
- createInvestment함수가 만들어 준 자원은 그 자원을 관리할 auto_ptr 객체를 초기화하는데 사용되고 있다. 이런 아이디어는 업계 용어로도 자주 통용되는데 자원 획득 즉 초기화(Resource Acquisition Is Initialization: RAII)이다. 자원을 획득하고 나서 바로 자원 관리 객체에 넘겨 준다.
- 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다.
- 소멸자는 어떤 객체가 소멸될 때(유효범위를 벗어나는 경우가 한 가지 예) 자동적으로 호출되기 때문에, 실행 제어가 어떤 경위로 블록을 떠나는가에 상관없이 자원 해제가 제대로 이루어지게 된다.
auto_ptr은 자신이 소멸될 때 자신이 가리키고 있는 대상에 대해 자동으로 delete를 먹이기 때문에, 어떤 객체를 가리키는 auto_ptr의 개수가 둘 이상이면 안 된다. 이러한 사태가 되면 자원이 두 번 삭제되며 미정의 동작이 된다. 이런 불상사를 막기 위해 auto_ptr 객체를 복사하면(복사 생성자 혹은 복사 대입 연산자) 원본 객체는 null로 만든다.
std::auto_ptr<Investment> pInv1(createInvestment());
std::auto_ptr<Investment> pInv2(pInv1); // pInv2가 pInv1이 가리키고 있던 객체를 가리키고
// pInv1은 null이다
pInv1 = pInv2; // pInv1이 pInv2가 가리키고 있던 객체를 가리키고
// pInv2는 null이다.
동적으로 할당되는 모든 자원에 대한 관리 객체로서 auto_ptr을 쓰는 것은 최선이 아니다.
shared_ptr
참조 카운팅 방식 스마트 포인터(Reference-Counting Smart Pointer: RCSP)는 특정한 어떤 자원을 가리키는(참조하는) 외부 객체의 개수를 유지하고 있다가 그 개수가 0이 되면 해당 자원을 자동으로 삭제하는 스마트 포인터이다. 단 참조 상태가 고리를 이루는 경우(예를 들어 다른 두 객체가 서로를 가리키고 있다든지)를 없앨 수 없다.
TR1에서 제공하는 tr1:shared_ptr이 대표적인 RCSP이다.
void f()
{
tr1::shared_ptr<Investment> pInv(createInvestment());
... // pInv 사용
} // shared_ptr의 소멸자를 통해
// pInv를 삭제
auto_ptr과 거의 똑같아 보이지만 복사가 훨씬 자연스럽다.
{
tr1::shared_ptr<Investment> pInv1(createInvestment());
tr1::shared_ptr<Investment> pInv2(pInv1); // pInv2가 pInv1이 가리키고 있던 객체를 가리키고
// 동시에 pInv1도 가리킨다.
pInv1 = pInv2; // 변한 것은 없다.
} // pInv1과 pInv2가 소멸되며,
// 이들이 가리키고 있는 객체도 자동 삭제
auto_ptr 및 shared_ptr은 소멸자 내부에서 delete 연산자를 사용한다. delete[ ] 연산자가 아니다. 동적으로 할당한 배열에 대해 auto_ptr과 shared_ptr을 사용하면 난감하다. 심지어 컴파일 에러도 발생하지 않는다.
위의 createInvestment 함수의 반환 타입이 포인터로 되어 있는데 이 부분 때문에 문제가 발생할 수 있다. 반환된 포인터에 대한 delete 호출을 호출자 쪽에서 해야 하는데, 그것을 잊어버리고 넘어가기 쉬워지기 때문이다.
이것만은 잊지 말자!
- 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는 RAII 객체를 사용하자.
- 일반적으로 널리 쓰이는 RAII 클래스는 std::auto_ptr 그리고 tr1::shared_ptr이다. tr1::shared_ptr이 복사 시의 동작이 직관적이기 때문에 대개 더 좋다. 반면, auto_ptr은 복사되는 객체(원본 객체)를 null로 만들어 버린다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 15: 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자 (2) | 2023.10.12 |
---|---|
[Effective C++] 14: 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자 (1) | 2023.10.10 |
[Effective C++] 12: 객체의 모든 부분을 빠짐없이 복사하자 (0) | 2023.10.08 |
[Effective C++] 11: operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자 (0) | 2023.10.07 |
[Effective C++] 10: 대입 연산자는 *this의 참조자를 반환하게 하자 (0) | 2023.10.06 |