Notice
Recent Posts
Recent Comments
Link
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Archives
Today
Total
관리 메뉴

로또

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

책/Effective C++

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

아롱로또 2023. 11. 11. 15:33

다루는 내용

RAII 클래스와 Lock을 기반으로 복사 동작에 대해 취할 수 있는 방법을 알아보자.

 

RAII를 이용한 Mutex 관리 클래스

RAII를 이용해 mutex를 관리하는 클래스를 제작하였다.

즉, 생성 시 자원을 획득하고, 소멸 시 그 자원을 해제하는 것이다.

#include <iostream>
#include <mutex>

using namespace std;

class Lock{
public:
    explicit Lock(mutex* pm): mutex_ptr(pm){
        mutex_ptr->lock();
        cout << "Lock 성공" << endl;
    }
    ~Lock(){
        mutex_ptr->unlock();
        cout << "UnLock 성공" << endl;

    }
private:
    mutex* mutex_ptr;
};

int main(){
    mutex m;
    {
        Lock m1(&m); // lock 획득
        // ... 자원 사용
    } // 소멸자에 의한 자원 자동 unlock
}

실행 결과

위와 같이 자원 관리 클래스를 제작하고 사용한다면 문제가 없어보인다.

하지만 만약 Lock 객체가 복사된다면 어떻게 처리해야 할까?

 

RAII 객체가 복사될 때 어떤 동작이 이루어져야 할까?

1. 복사를 금지한다.

위의 Lock 클래스 처럼, 실제로 RAII 객체가 복사되도록 두는 것 자체가 말이 안되는 경우가 많다. 복사하면 안 되는 RAII 클래스에 대해서 복사가 되지 않도록 다음과 같이 항목 6에서 다루었던 복사 방지 클래스를 이용할 수 있다.

// 복사 방지 클래스
class UnCopyable{
// 자식에 대해 생성과 소멸 허용
protected:
    UnCopyable(){}
    ~UnCopyable(){}

private:
    UnCopyable(const UnCopyable&); // 복사 생성자 선언
    UnCopyable& operator=(const UnCopyable&); // 대입 연산자 선언
};

class Lock: private UnCopyable{
public:
    explicit Lock(Mutex* pm): mutex_ptr(pm){
        lock(mutex_ptr);
    }
    ~Lock(){
        unlock(mutex_ptr);
    }
private:
    Mutex* mutex_ptr;
};

2. 관리하고 있는 자원에 대해 참조 카운팅을 수행하자.

자원을 사용하고 있는 마지막 객체가 소멸될 때까지 해당 자원을 해제하지 않는 것이 바람직한 경우도 있다. 이러한 경우에는 해당 자원을 참조하는 객체의 개수에 대해 카운트를 증가시키는 방식으로 RAII 객체의 복사 동작을 구현해야 한다.

 

이러한 방식은 스마트 포인터 중 하나인 shared_ptr와 동일한데, 이를 이용하여 자신이 만든 RAII 클래스에 shared_ptr를 데이터 멤버로 사용하면 간단히 구현할 수 있다.

단, shared_ptr은 참조 카운트가 0이 될 때, 자신이 가리키는 대상을 삭제하도록 기본적으로 동작한다. 우리는 mutex를 삭제하는 것이 아니라 잠금만 해제할 것이므로 shared_ptr가 삭제자(deleter) 지정을 허용한다는 점을 이용할 수 있다.

삭제자란, shared_ptr이 갖는 참조 카운트가 0이될 때 호출되는 함수 혹은 함수 객체를 말한다. shared_ptr의 두 번째 매개변수로 선택적으로 추가해줄 수 있다.

#include <iostream>
#include <mutex>

using namespace std;

// 삭제자
void deleter(mutex* mutex_ptr){
    mutex_ptr->unlock();
    cout << "deleter - UnLock 성공" << endl;
}

class Lock{
public:
    explicit Lock(mutex* pm): mutex_ptr(pm, deleter){ // 삭제자 전달
        mutex_ptr->lock();
        cout << "Lock 성공" << endl;
    }
    // 소멸자 생성을 잊은게 아닌, 컴파일러가 생성한 소멸자 사용.
private:
    shared_ptr<mutex> mutex_ptr; // shared_ptr로 변경
};

int main(){
    mutex m;
    {
        Lock m1(&m); // lock 획득
        // ... 자원 사용
    } // 소멸자에 의한 자원 자동 unlock
}

deleter를 이용한 자원 해제

 

3. 관리하고 있는 자원을 진짜로 복사한다.

때에 따라서는 자원을 원하는 대로 복사할 수도 있다. 이 경우에는 "자원을 다 썼을 때, 각각의 사본을 확실히 해제하는 것"이 자원 관리 클래스가 필요한 유일한 명분이다.

자원 관리 객체를 복사하면 해당 객체가 둘러싸고 있는 자원까지 복사하는 깊은 복사를 수행해야 한다.

4. 관리하고 있는 자원의 소유권을 옮기자.

특정한 자원에 대해서 해당 자원을 실제로 참조하는 RAII 객체는 오직 하나만 존재하도록 만들고 싶은 경우에 해당한다. 이러한 경우, RAII 객체가 복사될 때 기존의 RAII 객체는 해당 자원의 소유권을 사본에게 전달하고 해당 자원의 소유권을 잃는다. 스마트 포인터 중 하나인 unique_ptr가 이에 해당한다.

이것만은 잊지 말자!

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