14: 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자
세상의 자원은 모두 힙에서 생성되지는 않는다. 힙에 생기지 않는 자원은 auto_ptr 혹은 shared_ptr 등의 스마트 포인터로 처리해 주기엔 맞지 않다는 것이 일반적인 견해이다.
Mutex 타입의 뮤텍스 객체를 조작하는 C API를 사용하고 있다고 가정하자. 이 C API에서 제공하는 함수 중엔 lock 및 unlock이 있다.
void lock(Mutex* pm); // pm이 가리키는 뮤텍스에 잠금을 건다.
void unlock(Mutex* pm); // pm이 가리키는 뮤텍스의 잠금을 푼다.
이 뮤텍스 잠금을 관리하는 클래스를 하나 만들어 보자. 이전에 걸어 놓은 뮤텍스의 잠금을 잊지 않고 풀어 줄 목적이다. 이런 용도의 클래스는 기본적으로 RAII 법칙을 따라 구성한다. 생성 시, 자원을 획득하고 소멸 시에 그 자원을 해제한다.
class Lock {
public:
explicit Lock(Mutex* pm)
: mutexPtr(pm)
{
lock(mutexPtr);
}
~Lock()
{
unlock(mutexPtr);
}
private:
Mutex* mutexPtr
};
Mutex m;
{
Lock m1(&m);
}
그런데 Lock 객체가 복사된다면 어떻게 해야 할까? RAII 객체가 복사될 때 어떤 동작이 이루어져야 할까?
복사를 금지한다.
실제로 RAII 객체가 복사되도록 놔두는 것 자체가 말이 안 되는 경우가 꽤 많다. 어떤 스레드 동기화 객체에 대한 사본이라는 게 실제로 거의 의미가 없으니까. 복사하면 안 되는 RAII 클래스에 대해서는 반드시 복사가 되지 않도록 막아야 한다. 복사 함수를 private 멤버로 만들면 된다.
관리하고 있는 자원에 대해 참조 카운팅을 수행한다.
자원을 사용하고 있는 마지막 객체가 소멸될 때까지 그 자원을 저 세상으로 안 보내는 게 바람직할 경우도 종종 있다. 이럴 경우에는, 해당 자원을 참조하는 객체의 개수에 대한 카운팅을 증가시키는 식으로 해야 한다. shared_ptr이 이런 방식으로 사용된다.
위의 코드에서 MutexPtr의 타입을 Mutex* 에서 tr1::shared_ptr<Mutex>로 바꾸는 것이다. 단, shared_ptr은 참조 카운트가 0이 될 때, 자신이 가리키고 있던 객체를 삭제해 버리도록 기본 동작이 만들어져 있기에 삭제자(deleter)를 지정해야 한다. 삭제자는 tr1:shared_ptr 생성자의 두 번째 매개변수로 선택적으로 넣어 줄 수 있다.
class Lock {
public:
explicit Lock(Mutex* pm)
: mutexPtr(pm, unlock) // 삭제자로 unlock함수를 이용한다.
{
lock(mutexPtr.get());
}
private:
tr1::shared_ptr<Mutex> mutexPtr; // 원시 포인터 대신 shared_ptr을 사용한다.
};
위의 코드에서 주목해야 할 점은 Lock 클래스가 이제는 소멸자를 선언하지 않는다는 점이다. 비정적 데이터 멤버인 mutexPtr의 소멸자가 자동으로 생성된 Lock 클래스의 소멸자에서 호출된다. mutexPtr의 소멸자는 뮤텍스의 참조 카운트가 0이 될 때, shared_ptr의 삭제자(여기서는 unlock)을 자동으로 호출한다.
관리하고 있는 자원을 진짜로 복사한다.
때에 따라서는 자원을 원하는 대로 복사할 수도 있다. 이때는 '자원을 다 썼을 때 각각의 사본을 확실히 해제하는 것'이 자원관리 클래스가 필요한 유일한 명분이 된다. 자원 관리 객체를 복사하면 그 객체가 둘러싸고 있는 자원까지 복사되어야 한다. 즉, 깊은 복사(deep copy)를 수행해야 한다.
관리하고 있는 자원의 소유권을 옮긴다.
어떤 특정한 자원에 대해 그 자원을 실제로 참조하는 RAII 객체는 딱 하나만 존재하도록 만들고 싶어서, 그 RAII 객체가 복사될 때 그 자원의 소유권을 사본 쪽으로 아예 옮겨야 할 경우도 있다. 이런 스타일의 복사가 auto_ptr의 복사 이다.
이것만은 잊지 말자!
- RAII 객체의 복사는 그 객체가 관리하는 자원의 복사 문제를 안고 가기 때문에, 그 자원을 어떻게 복사하느냐에 따라 RAII 객체의 복사 동작이 결정된다.
- RAII 클래스에 구현하는 일반적인 복사 동작은 복사를 금지하거나 참조 카운팅을 해 주는 선으로 마무리하는 것이다. 하지만 이 외의 방법들도 가능하니 잠고해 두자.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 16: new 및 delete를 사용할 때는 형태를 반드시 갖추자 (1) | 2023.10.13 |
---|---|
[Effective C++] 15: 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자 (2) | 2023.10.12 |
[Effective C++] 13: 자원 관리에는 객체가 그만! (0) | 2023.10.09 |
[Effective C++] 12: 객체의 모든 부분을 빠짐없이 복사하자 (0) | 2023.10.08 |
[Effective C++] 11: operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자 (0) | 2023.10.07 |