항목 14 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자
자 우리는 항목 13에서 RAII 기법을 이용해 힙 영역의 자원을 관리하는 방법에 대해 배웠다. 이 예로 우리는 auto_ptr과 shared_ptr에 대해서 배웠는데 경우에 따라서는 우리가 직접 자원 관리 클래스를 만들어야 하는 경우도 생긴다. 아래 예시를 한 번 보자. Mutex 타입의 뮤텍스 객체를 조작하는 C API를 사용하고 있다고 가정하자. 이 C API가 제공하는 함수 중에는 뮤텍스를 잠그는 lock 및 잠금을 푸는 unlock 함수가 있다.
void lock(Mutex* pm);
void unlock(Mutex* pm);
그런데 나는 여기서 뮤텍스의 잠금을 자동으로 관리해주는 클래스를 하나 만들고 싶다. 그래서 생성시에 자원을 획득하고 소멸시에 자원을 해제하도록 다음과 같이 설계했다.
class Lock {
public:
explicit Lock(Mutex* pm)
: mutextPtr(pm)
{ lock(mutextPtr); }
~Lock() { unlock(mutextPtr); }
private:
Mutex* mutextPtr;
};
그래서 우리는 사용시에 RAII방식으로 사용하면 된다.
Mutex m; // 뮤텍스 정의
...
{ // 임계 영역을 정하기 위한 블록 생성
Lock m1(&m); // 뮤텍스에 잠금을 검
...
} // 블록이 끝나면 자동으로 뮤텍스가 풀림
자 이렇게까지만 보면 아무런 문제가 없어 보인다. 하지만 실수로 Lock 객체가 복사된다면 어떻게 되어야 할까?
Lock m1(&m); // m에 잠금을 검
Lock m2(m1); // ??? 어떤 행동을 해야 할까?
자, 여기서 선택지는 여러가지가 있다. 하나씩 알아보도록 하자.
첫 번째 방법은 복사를 아예 금지하는 것이다. 복사를 금지하는 것은 우리가 항목6에서 이미 배워서 알것이다. 바로 Uncopyable 클래스를 만들고 상속 시키면 될 것이다.
class Lock : private Uncopyable {
public:
...
};
두 번째 방법은 바로 관리하고 있는 자원에 대해 참조 카운팅을 수행하는 것이다. 우리가 배운 shared_ptr이 이러한 방식을 사용하고 있다. 그런데 우리가 굳이 이런 참조 카운팅 방식을 구현할 필요 없이 이미 잘 구현 되어있는 shared_ptr을 멤버로 두면 되지 않을까? 하지만 문제가 있다. 우리는 단순히 Mutex를 해제하려는 것이지 메모리를 없애 버리려는 것이 아니다. 그렇다면 어떻게 해야 좋을까? 다행스럽게도 shared_ptr은 삭제자(deleter)를 지정 할 수 있다. 아래 코드 처럼 함수 포인터나 람다를 이용해서 지정할 수 있다.
void Deleter(Mutex* pm)
{
unlock(pm);
}
class Lock {
public:
explicit Lock(Mutex* pm)
: mutexPtr(pm, Deleter)
{
lock(mutexPtr.get());
}
private:
std::shared_ptr<Mutex> mutexPtr;
};
다음으로 세 번째 방법은 자원을 진짜로 복사해버리는 것이다. 단순히 같은 Mutex를 가리키도록 하는 것이 아닌 새로운 Mutex를 생성하여 새로운 힙 메모리를 할당하는 깊은 복사(deep copy)를 해버리는 것도 방법이다. 마지막으로 네 번째 방법은 관리하고 있는 자원의 소유권을 옮기는 것이다. 마치 auto_ptr처럼 말이다.