항목 13 : 자원 관리에는 객체가 그만!
투자를 모델링해 주는 클래스 라이브러리를 가지고 어떤 작업을 한다고 가정합시다. 이 라이브러리는 Investment라는 최상위 클래스가 있고, 이것을 기본으로 하여 구체적인 형태의 투자 클래스가 파생되어 있습니다.
1
|
class Investment { ... }; // 여러 형태의 투자를 모델링한 클래스 계통의 최상위 클래스
|
가정을 하나 더 하겠습니다. 이 라이브러리는 Investment에서 파생된 클래스의 객체를 사용자가 얻어내는 용도로 팩토리 함수만을 쓰도록 만들어져 있다고요.
1
2
|
Investment* createInvestment(); // Investment 클래스 계통에 속한 클래스의 객체를 동적 할당하고 그 포인터를 반환합니다.
// 이 객체의 해제는 호출자 쪽에서 직접 해야 합니다.
Colored by Color Scripter
|
주석문에 나와 있듯이, createInvestment 함수를 통해 얻어낸 객체를 사용할 일이 없을 때 그 객체를 삭제해야 하는 쪽은 이 함수의 호출자 입니다.
아래의 함수는 그렇게 만들었습니다.
1
2
3
4
5
6
7
8
|
void f()
{
Investment *pInv = createInvestment(); // 팩토리 함수를 호출합니다.
... // pInv를 사용합니다.
delete pInv; // 객체를 해제합니다.
}
Colored by Color Scripter
|
createInvestment 함수로부터 얻는 투자 객체의 삭제에 실패할 수 있는 경우가 한두 가지가 아닙니다.
createInvestment 함수로 얻어낸 자원이 항상 해제되도록 만들 방법은, 자원을 객체에 넣고 그 자원 해제를 소멸자가 맡도록 하며, 그 소멸자는 실행 제어가 f를 떠날 때 호출되도록 만드는 것입니다. 자원을 객체에 넣음으로써, C++가 자동으로 호출해 주는 소멸자에 의해 해당 자원을 저절로 해제할 수 있습니다.
소프트웨어 개발에 쓰이는 상당수의 자원이 힙에서 동적으로 할당되고, 하나의 블록 혹은 함수 안에서만 쓰이는 경우가 잦기 때문에 그 블록 혹은 함수로부터 실행 제어가 빠져 나올 때 자원이 해제되는게 맞습니다. 효준 라이브러리를 보면 auto_ptr이란 것이 있는데, 바로 이런 용도에 쓰라고 마련된 클래스입니다. auto_ptr은 포인터와 비슷하게 동작 하는 객체[스마트 포인터]로서, 가리키고 있는 대상에 대해 소멸자가 자동으로 delete를 불러주도록 설계되어 있습니다. 그럼 f에서 생길 수 있는 자원 누출을 막기 위해 auto_ptr을 사용하는 방법을 보시겠습니다.
1
2
3
4
5
|
void f()
{
std::auto_ptr pInv(createInvestment()); // 팩토리 함수를 호출합니다.
... // 예전처럼 pInv를 사용합니다.
} // auto_ptr의 소멸자를 통해 pInv를 삭제합니다.
|
자원 관리에 객체를 사용하는 방법의 중요한 두 가지 특징
1. 자원을 획득한 후에 자원 관리 객체에게 넘긴다.
2. 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다.
첫 번째, 자원을 획득한 후에 자원 관리 객체에게 넘긴다.
위의 예제를 보면, create-Investment 함수가 만들어 준 자원은 그 자원을 관리할 auto_ptr 객체를 초기화 하는데 쓰이고 있습니다.
실제로, 이렇게 자원 관리에 객체를 사용하는 아이디어에 대한 업계 용어도 자주 토용되고 있는데, 자원 획득 즉 초기화(Resource Acquisition Is Initalization : RAII)라는 이름입니다.
이런 이름이 나온 이유는 자원 획득과 자원 관리 객체의 초기화가 바로 한 문장에서 이루어지는 것이 너무나도 일상적이기 때문입니다. 획득된 자원으로 자원 관리 객체를 초기화하지 않고 그 자원을 그 객체에 대입하는 경우도 종종 있기는 하지만, 어찌 됐든 "자원을 획득하고 나서 바로 자원 관리 객체에 넘겨 준다"는 점은 같습니다.
두 번째, 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 합니다.
소멸자는 어떤 객체가 소멸될 때(유효범위를 벗어나는 경우가 한 가지 예) 자동적으로 호출되기 때문에, 실행 제어가 어떤 경위로 블록을 떠나는가에 상관없이 자원 해제가 제대로 이루어지게 되는 것입니다. 물론 객체를 해제하다가 예외가 발생될 수 있는 상황에 빠지면 사태가 많이 꼬이기도 하겠지만, 그 문제는 항목 8에서 해결할 부분이기 때문에 여기서 걱정할 필요는 없겠습니다.
auto_ptr은 자신이 소멸될 때 자신이 가리키고 있는 대상에 대해 자동으로 delete를 먹이기 때문에, 어떤 객체를 가리키는 auto_ptr의 개수가 둘 이상이면 절대로 안 되겠지요. 만에 하나 이러한 사태가 되면 결국 자원이 두 번 삭제되는 결과를 낳게 되고, 프로그램은 미정의 동작의 수렁에 빠지게 될 테니까요. 이런 불상사를 막는답시고 auto_ptr은 상당히 유별난 특성을 지니게 되었는데, 그게 무엇인고 하니 auto_ptr 객체를 복사하면 원본 객체는 null로 만든답니다.
1
2
3
4
5
|
std::auto_ptr pInv1(createInvestment()); // pInv1이 가리키는 대상은 createInvestment 함수에서 반환된 객체입니다.
std::auto_ptr pInv2(pInv1); // pInv2는 현재 그 객체를 가리키고 있는 반면, pInv1은 null입니다.
pInv1 = pInv2; // 지금 pInv1은 그 객체를 가리키고 있으며, pInv2는 null입니다.
|
STL 컨테이너의 경우엔 원소들이 '정상적인' 복사 동작을 가져야 하기 때문에, auto_ptr은 이들의 원소로 허용되지 않습니다.
auto_ptr을 쓸 수 없는 상황이라며느 그 대안으로 참조 카운팅 방식 스마트 포인터(reference-counting smart pointer : RCSP)가 아주 괜찮습니다. RCSP는 특정한 어떤 자원을 가리키는 외부 객체의 개수를 유지하고 있다가 그 개수가 0이 되면 해당 자원을 자동으로 삭제하는 스마트 포인터입니다. 이것만 보면, RCSP의 동작은 가비지 컬렉션의 그것과 상당히 흡사합니다. 단, 참조 상태가 고리를 이루는 경우(예를 들면 다른 두 객체가 서로를 가리키고 있다든지)를 없앨 수 없다는 점은 가비지 컬렉션과 다릅니다.
TR1에서 제공되는 tr1::shared_ptr이 대표적인 RCSP입니다.
1
2
3
4
5
6
|
void f()
{
...
std::tr1::shared_ptr pInv(createInvestment()); // 팩토리 함수를 호출합니다.
... // 예전처럼 pInv를 사용합니다.
} // shared_ptr의 소멸자를 통해 pInv를 자동으로 삭제합니다.
|
auto_ptr을 사용한 버전과 거의 똑같아 보이는 코드이지만, shared_ptr의 복사가 훨씬 자연스러워졌습니다.
1
2
3
4
5
6
7
8
9
10
|
void f()
{
...
std::tr1::shared_ptr pInv1(createInvestment()); // pInv1이 가리키는 대상은 createInvestment 함수에서 반환된 객체입니다.
std::tr1::shared_ptr pInv2(pInv1); // pInv1 및 pInv2가 동시에 그 객체를 가리키고 있습니다.
pInv1 = pInv2; // 마찬가지 변한 것은 하나도 없습니다.
...
} // pInv1 및 pInv2는 소멸되며, 이들이 가리키고 있는 객체도 자동으로 삭제됩니다.
Colored by Color Scripter
|
복사 동작이 '예상대로' 이루어지기 때문에, tr1::shared_ptr은 괴상한 복사 동작으로 인해 auto_ptr을 쓸 수 없는 STL 컨테이너 등의 환경에 딱 맞게 쓸 수 있습니다.
알아두셔야 할 게 하나 더 있습니다. auto_prt 및 tr1::shared_ptr은 소멸자 내부에서 delete 연산자를 사용합니다 delete[] 연산자가 아닙니다. 말하자면, 동적으로 할당한 배열에 대해 auto_ptr이나 tr1::shared_ptr을 사용하면 난감하다는 이야기입니다.
꼭 잊지 말아야 할 것!
1. 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는 RAII 객체를 사용합시다.
2. 일반적으로 널리 쓰이는 RAII 클래스 tr1::shared_ptr 그리고 auto_ptr입니다. 이 둘 가운데 tr1::shared_ptr이 복사 시의 동작이 직관적이기 때문에 대개 더 좋습니
다. 반면, auto_ptr은 복사되는 객체(원본 객체)를 null로 만들어 버립니다.
'언어 > C++' 카테고리의 다른 글
[Effective C++] 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자 (0) | 2020.01.15 |
---|---|
[Effective C++] 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자 (0) | 2020.01.14 |
[Effective C++] 객체의 모든 부분을 빠짐없이 복사하자 (0) | 2020.01.12 |
[Effective C++] operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자 (0) | 2020.01.11 |
[Effective C++] 대입 연산자는 *this의 참조자를 반환하게 하자 (0) | 2020.01.10 |