Effective C++/Chapter 3: 자원관리

[Effective C++] 항목 14: 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자

yeoul0714 2025. 4. 29. 05:00

1. 개요


이번 항목은 자원관리 클래스를 만들때 복사 동작에 대해서 어떻게 처리해야할지를 다루는 문서입니다.

 

예를 하나 들어보도록 하겠습니다.

void lock(Mutex* pm); // pm이 가리키는 mutex에 잠금을 겁니다.
void unlock(Mutex* pm); //pm이 가리키는 mutex에 잠금을 풉니다.

 

이러한 mutex잠금을 관리하는 클래스를 만들려고 합니다.

 

이러한 클래스 역시 RAII 법칙을 따르게 됩니다.

 

생성시 자원을 획득하고 소멸 시에 자원을 해제하는 개념입니다.

 

아래는 이러한 클래스의 예제입니다.

class Lock {
public:
    explicit Lock(Mutex *pm) : mutexPtr(pm) { 
        lock(mutexPtr);               // 자원 획득
    }
    ~Lock() { 
        unlock(mutexPtr);             // 자원 해제
    }
private:
    Mutex *mutexPtr;
};

 

이러한 방식으로 lock을 걸 수 있습니다.

 

이 방식은 아주 정상적인 방식입니다.

Mutex m;                              // 사용할 뮤텍스 정의
...
{                                     // 임계 영역을 정의하는 블록 생성
    Lock ml(&m);                      // 뮤텍스 잠금
    ...                               // 임계 영역 작업 수행
}                                     // 블록 끝에서 자동으로 뮤텍스 잠금 해제

 

그렇다면 이런식으로 복사가 일어난다면 어떻게 되는 것일까요?

Lock ml1(&m);                         // m을 잠금
Lock ml2(ml1);                        // ml1을 ml2로 복사 - 여기서 무슨 일이 일어나야 할까요?

 

2. 복사에 대응하는 방법들


 

2-1. 복사를 금지한다

사실상 복사를 하는 것 자체가 말이 안되는 경우가 대부분입니다.

 

Lock 클래스로 이와같은 경우입니다.

 

스레드의 동기화 객체에 대해서 복사를 한다는 것이 의미가 없습니다.

왜 Lock 클래스의 복사가 의미가 없는지?

Lock 클래스는 뮤텍스(Mutex)라는 동기화 자원을 관리합니다. 이 클래스의 목적을 생각해 봅시다:

  1. Lock의 목적: Lock 객체는 임계 영역(Critical Section)에 들어갈 때 뮤텍스를 잠그고, 임계 영역에서 나올 때 자동으로 뮤텍스를 해제하는 것입니다.
  2. 뮤텍스의 본질: 뮤텍스는 한 번에 하나의 스레드만 특정 자원에 접근할 수 있도록 보장하는 동기화 메커니즘입니다.

 

복사할 때 일어날 수 있는 시나리오

  1. 동일한 뮤텍스에 대한 포인터 복사: ml2도 ml1과 같은 뮤텍스(m)를 가리키도록 합니다.
    • 문제점: 이미 잠긴 뮤텍스를 ml2가 또 잠그려고 하면 데드락이 발생할 수 있습니다.
    • 문제점: ml2가 소멸될 때 뮤텍스를 해제하고, 그 후 ml1이 소멸될 때 또 해제하려고 하면 이중 해제 오류가 발생합니다.
  2. 새로운 뮤텍스 생성: ml2를 위해 새 뮤텍스를 생성합니다.
    • 문제점: 원래의 목적(특정 뮤텍스 m을 관리하는 것)과 맞지 않습니다.
    • 문제점: 실제로 임계 영역을 보호하는 것은 원래의 뮤텍스 m이므로, 새 뮤텍스는 의미가 없습니다.

이런 이유로 Lock 클래스의 복사는 논리적으로 말이 되지 않습니다.

 

한 임계 영역에서 하나의 뮤텍스를 한 번 잠그는 것이 목적인데, 복사를 허용하면 이 의도가 훼손됩니다.

 

그러므로 이런 경우엔 복사를 막아줘야 합니다. (항목 6 참고) 

https://yeoul0714.tistory.com/32

 

[Effective C++] 항목 6: 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자

1. 개요블로그의 주인장인 여울은 게임을 사랑하는 개발자이다. 여울은 게임을 소개하는 프로그램을 만들었데 이 프로그램에는 게임을 나타내는 클래스가 있다.class Game {}; 그런데 이세상에 똑

yeoul0714.tistory.com

 

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

이러한 방식은 사실 우리가 이전에 공부했던 shared_ptr이 하고있는 방식입니다.

 

이러한 자원관리 클래스에 복사 동작을 넣고 싶다면 shared_ptr을 데이터 멤버변수로 넣으면 해결 될것입니다.

 

Mutex*에서 shared_ptr<Mutex>로 바꾸는 것이지요

 

그러나

 

shared_ptr은 참조 카운트가 0이되면 자원을 해제합니다.

 

그러나 우리는 mutex를 삭제하고 싶은 것이 아니라 unlock을 하고싶은 것입니다.

 

놀랍게도 shared_ptr은 deleter를 지정하는 것이 가능합니다.

 

Lock 클래스의 자동 생성 소멸자가 호출될 때 멤버 변수인 shared_ptr이 소멸되면서 참조 카운트가 0이 되고 

 

그때 우리가 지정한 deleter(unlock)가 호출됩니다.

 

class Lock {
public:
   explicit Lock(Mutex *pm)          // Mutex를 가리키는 shared_ptr 초기화
   : mutexPtr(pm, unlock)            // 가리킬 포인터와 삭제자 함수 unlock 지정
   {                                  
       lock(mutexPtr.get());         // shared_ptr에서 원시 포인터를 얻기 위해 get() 메서드 사용
   }
private:
   std::tr1::shared_ptr<Mutex> mutexPtr;  // 원시 포인터 대신
};                                          // shared_ptr 사용

이런식으로 shared_ptr로 선언해주고 초기화 리스트에서 unlock이라는 deleter로 초기화를 해주었습니다.

 

이제 mutex는 삭제되지 않고 deleter로 지정해준 unlock이 실행되며 lock이 해제 될 것입니다.

 

눈여겨 볼 점은 소멸자를 선언해주지 않았다는 것입니다.

 

여기서는 클래스가 자동으로 생성해주는 소멸자가 호출됩니다. 

 

그 소멸자는 바로 우리가 지정해준 deleter이고요

 

주석으로 자동으로 생성되는 deleter에 의해서 소멸된다고 써주면 아주 좋은 코드가 됩니다.

 

2-3. 관리하고 있는 자원을 진짜로 복사합니다.

때에 따라서는 복사할 수도 있습니다.

 

자원 관리 객체를 복사한다면 그 객체가 둘러 싸고있는 모든 자원을 복사해야 합니다.

 

즉 깊은 복사를 해야한다는 것이죠

 

어떤 구현 환경에서는 string의 원소들을 heap에 저장하고 이 메모리에 대한 포인터를 데이터 멤버로 가지고 있습니다.

 

이 string을 복사하게 되면 사본은 새로운 heap메모리와 그 메모리를 가리키는 새로운 포인터를 가지게 됩니다.

 

이것이 깊은 복사를 보여주는 하나의 예시입니다.

 

2-4. 관리하고 있는 자원의 소유권을 옮깁니다.

어떤 자원을 참조하는 RAII 객체를 딱 하나만 만들고 싶다면 복사를 할때

 

자원의 소유권을 사본으로 옮겨야 할 경우도 생깁니다.

 

+ 과거엔 auto_ptr로 이것을 구현했지만 C++11부터 unique_ptr이 도입되었습니다.

https://yeoul0714.tistory.com/46

 

[C++] Smart Pointer

https://www.inflearn.com/course/%EC%96%B8%EB%A6%AC%EC%96%BC-3d-mmorpg-1/dashboard [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part1: C++ 프로그래밍 입문 강의 | Rookiss - 인프런Rookiss | , MMORPG 개발에 필요한 모든 기

yeoul0714.tistory.com

 

 

이것만은 잊지 말자!

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

https://yeoul0714.tistory.com/49

 

[Effective C++] 항목 15: 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자

1. 개요자원 관리 클래스는 메모리 누수를 막는 아주 훌륭한 방법입니다. 그러나 우리는 종종 자원 관리 객체의 보호벽을 넘어서 실제 자원을 직접 만져야 할 일이 종종 생길것입니다. std::tr1::sha

yeoul0714.tistory.com