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

Jangmanbo·2023년 3월 26일
0

Effective C++

목록 보기
14/26

힙에서 생기는 자원들은 항목13에서 언급한 unique_ptr, shared_ptr 같은 스마트 포인터로 자원을 관리하면 된다. 그러나 힙이 아닌 공간에서 생성된 자원은 스마트 포인터로 처리하기 적합하지 않다. 이럴 때는 스스로 자원 관리 클래스를 만들어야 한다.

자원 관리 클래스 만들기

void lock(Mutex *pm);	// pm이 가리키는 뮤텍스에 잠금을 건다.
void unlock(Mutex *pm);	// pm이 가리키는 해당 뮤텍스의 잠금을 푼다.

Mutex 타입의 뮤텍스 객체를 조작하는 C API를 사용 중이라고 가정한다. RAII(생성 시 자원을 획득하고 소멸 시 그 자원을 해제) 법칙에 따라 이 뮤텍스 잠금을 관리하는 클래스를 구성한다.

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

RAII 법칙에 따라 잘 구성한 클래스처럼 보인다.

Lock ml1(&m);	// m에 잠금 걸기
Lock ml2(ml1);	// ml1을 ml2로 복사

그런데 이렇게 RAII 객체인 ml1을 복사할 때는 어떤 동작이 이루어져야 할까?

1. 복사 금지

RAII 객체를 복사하면 RAII의 의미가 없어질 수 있다. 위와 같이 Lock 클래스로 생성한 스레드 동기화 객체에 대해서는 사본이 존재하면 의미가 없다.
복사를 막는 방법은 항목 6에서 설명한다. (간단히 말하자면 객체 복사 함수를 private으로 선언)

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

해당 자원을 참조한 객체의 개수를 증가하는 식으로 복사 동작을 수행한다. 이런 경우 자원을 사용하고 있는 마지막 객체가 소멸될 때 자원을 해제한다. 대표적으로 shared_ptr이 있다.

2-1. RAII 클래스에 참조 카운팅 방식의 복사 동작을 넣고 싶다면

예를 들어 Lock클래스가 참조 카운팅 방식으로 동작하기를 원한다면 Mutex* mutexPtrshared_ptr<Mutex> mutexPtr로 바꾸면 되지 않을까? 라는 생각을 할 수 있다.

그러나 앞서 말했듯이 shared_ptr는 참조 카운트가 0이 되면 해당 객체를 삭제한다. 잠금 해제만을 원하는 Lock클래스에는 맞지 않는다.

2-2. 해결법: 삭제자를 지정하자!

삭제자 (deleter)

  • shared_ptr가 유지하는 참조 카운트가 0이 되었을 때 호출되는 함수 혹은 함수 객체
  • shared_ptr 생성자의 두 번째 매개변수로 선택적으로 넣을 수 있다.
class Lock {
public:
	// shared_ptr인 mutexPtr를 초기화할 때 삭제자로 unlock 함수를 지정한다.
	explicit Lock(Mutex* pm):mutexPtr(pm, unlock)
    {
    	lock(mutexPtr.get());	// 항목 15
    }

private:
	std::shared_ptr<Mutex> mutexPtr;	// 원시 포인터 대신 shared_ptr 사용
}

중요한 것은 바로 Lock클래스가 소멸자를 선언하지 않는다는 것이다.

소멸자는 비정적 데이터 멤버의 소멸자를 자동으로 호출한다. 그런데 이 비정적 데이터 멤버인 mutexPtr의 소멸자는 참조 카운트가 0이 될 때 삭제자인 unlock을 자동으로 호출한다.

따라서 굳이 Lock의 소멸자를 구현할 이유가 없는 것이다.

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

자원 관리 객체를 복사하려면 그 객체가 둘러싸고 있는 자원까지 모두 복사, 즉 깊은 복사를 수행해야 한다. 또한 자원을 다 썼을 때 각각의 사본을 확실히 해제해야 한다.

4. 관리하고 있는 자원의 소유권 이전

자원을 실제로 참조하는 RAII 객체를 딱 하나만 존재하도록 만들고 싶을 경우에는 자원의 소유권을 이전한다.


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



정리
1. RAII 객체의 복사는 그 객체가 관리하는 자원의 복사 문제도 안고 가기 때문에, 해당 자원을 어떻게 처리해야 할지 결정해야 한다.
2. RAII 클래스에 구현하는 일반적인 복사 동작은 복사를 금지하거나 참조 카운팅을 해주는 선이다.

0개의 댓글