Item 14: 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자

자원 관리 클래스의 주축을 이루는 자원 획득 즉 초기화 RAII 기법에 대해서 다뤘는데, 세상의 모든 자원은 동적 힙에서만 생기지는 않는다는 것이다.

Mutex타입의 뮤텍스 객체를 조작하는 C API를 사용중이라 가정한다.

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;
};

사용자는 Lock을 사용할 때 RAII 방식에 맞춰 쓰면 된다.

Mutex m;

...

{
    Lock ml(&m);
    // 뮤텍스를 잠금
}

// 뮤텍스 잠금 해제

RAII 객체가 복사될 때 어떤 동작이 일어나는가?

  • 복사를 금지한다.
    • 실제로 RAII 객체가 복사되도록 놔두는 것 자체가 말이 안되는 경우가 종종 있다. 위 Lock 같은 클래스도 이런 부류에 속하고 어떤 스레드 동기화 객체에 대한 ‘사본’이라는 게 실제로 거의 의미가 없다.
    • 복사하면 안 되는 RAII 클래스에 대해서는 반드시 복사가 되지 않도록 막아야 한다. Item 6 참고
  • 관리하고 있는 자원에 대해 참조 카운팅을 수행한다.
    • 자원을 사용하고 있는 마지막 객체가 소멸될 때까지 그 자원을 저 세상으로 안 보내는 게 바람직할 경우도 종종 있다. 이럴 경우에는, 해당 객체의 복사 동작을 만들어야 한다.
    • RAII 클래스에 참조 카운팅 방식의 복사 동작을 넣고 싶을 때 std::shared_ptr을 데이터 멤버로 넣으면 해결된다고 생각할 수 있지만 shared_ptr 은 참조 카운팅이 0이 되는 순간 자신이 가리키고 있던 대상을 삭제해 버리도록 기본 동작이 만들어져 있다. 잠금 해제가 필요하지 삭제가 필요하지는 않다.
    • 다만 std::shared_ptr이 삭제자(deleter)를 지정할 수 있도록 만들어져 있기 때문에 이를 이용해 잠금 해제를 수행하는 것이 가능하다. explicit Lock(Mutex* pm): mutexPtr(pm, unlock)와 같은 생성자를 만들어 놓으면 된다.
  • 관리하고 있는 자원을 진짜로 복사한다.
    • 때에 따라서는 자원을 원하는 대로 복사할 수도 있다. 이때는 자원을 다 썼을 때 각각의 사본을 확실히 해제하는 것이 자원 관리 클래스가 필요한 유일한 명분이 되는 것이다.
    • 자원 관리 객체를 복사하려면 그 객체가 둘러싸고 있는 자원까지 복사되어야 한다. deep copy를 수행해야 한다.
  • 관리하고 있는 자원의 소유권을 옮긴다.
    • 그리 흔한 경우는 아니지만, 특정한 자원에 대해 그 자원을 실제로 참조하는 RAII 객체는 딱 하나만 존재하도록 만들고 싶어서, 그 RAII 객체가 복사될 때 그 자원의 소유권을 사본 쪽으로 아예 옮겨야 할 경우도 있다.

정리

객체 복사 함수는 컴파일러에 의해 생성될 여지가 있기 때문에, 컴파일러가 생성한 버전의 동작이 원하는 바와 맞지 않는다면 직접 구현해야 한다.

  • RAII 객체의 복사는 그 객체가 관리하는 자원의 복사 문제를 안고 가기 때문에, 그 자원을 어떻게 복사하느냐에 따라 RAII 객체의 복사 동작이 결정된다.
  • RAII 클래스에 구현하는 일반적인 복사 동작은 복사를 금지하거나 참조 카운티을 해 주는 선으로 마무리하는 것이다. 하지만 이 외의 방법도 가능하니 참고하자.

태그: ,

카테고리:

업데이트:

댓글남기기