상세 컨텐츠

본문 제목

항목 14) 자원 관리 클래스의 복사 동작에 대해서

C++/Effective C++

by DeaKyungLee 2021. 12. 5. 14:13

본문

자원 관리 객체 핵심 복습

알다시피 자원 관리 클래스, 이른바 RAII 객체의 핵심은

  1. 자원 생성 시점에 초기화 ( Resource Acquisition is Initalizatiopn )
  2. 소멸자에서 해당 자원을 해제

위의 두가지 개념이다.

class Lock{
private:
	Mutex *mutexPtr;
public:
	explicit Lock( Mutex *pm ) : mutexPtr(pm) // explicit : 자동 형변환 방지
	{ lock(mutexPtr); }
 
 
	~Lock() { unlock(mutexPtr); }
}
 
 
...
Mutex m;			// 쓰고 싶은 뮤텍스 정의
...
{					// 임계영역 블록
	Lock m1(&m);	// 뮤텍스 잠금
	...
}

위의 코드를 보면 자원을 생성하는 시점에 lock 을 통해서 초기화 하고,

반대로 소멸자에서 해당 자원에 대해서 Unlock 을 진행한다.

사용자는 단순히 RAII 객체에 자원을 넘겨서 생성하고 작업하기만 하면 블록의 끝에서 자동으로 unlock이 진행된다.

 

자원 관리 객체의 복사

그런데 만약 m1 객체가 복사된다면 어떻게 될까?

해당 질문이 이번 장에서 묻고자 하는 핵심이다.

// RAII 복사
{
	...
	Lock m1(&m);
	Lock m2(m1);	// m1 을 복사하는 m2
	...
}

위의 코드는 컴파일러가 제지하지 않지만, 실행 중에 에러가 발생한다.

( 동일 mutex에 대해서 두번의 unlock 수행 )

RAII 객체와 같은 자원 관리 객체의 복사 행위를 Handling 하는 방식에는 크게 4가지가 존재한다.

1. 복사 자체를 금지한다.

→ 사실 자원 자체도 아닌 RAII 객체 자체가 복사된다는 상황 자체가 바람직하지 않다.

상황에 따라 다르겠지만 대부분의 경우에는 설계를 재검토 하거나 방식을 다시 생각하는 것이 바람직하다.

 항목 6) 에서 언급한 Uncopyable 방식처럼 복사 연산 관련 함수를 private 로 선언해버리는 방식이 가장 간단하고 효율적이다.

// UnCopyable

class UnCopyable {
protected:
    UnCopyable() { };
    ~UnCopyable() { };
 
private:
    // 선언만 함으로서 friend를 통한 호출도 막음
    UnCopyable(const UnCopyable&);
    UnCopyable& operator=(const UnCopyable&);
};
 
 
...
 
 
class Lock: private Uncopyable {
public:
	...
};

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

→ std::shared_ptr 처럼 하나의 자원을 참조하는 객체의 개수에 대한 카운트를 증감시키는 방식으로 관리하는 것

그런데 문제는 shared_ptr 의 동작 방식 자체가 참조 카운트가 0이 될 때 자신이 가리키는 자원을 삭제 시키는 방식으로 구현되어있다는 것이다.

따라서 위의 예시와 같이 mutex 의 경우에는 카운트 값이 0이 되는 순간에 unlock 아닌 해제를 진행하기 때문에 부적합하다고 할 수 있다.

이것을 보완하는 방식으로 shared_ptr 에서는 삭제자(deleter) 지정을 허용한다는 점을 활용할 수 있다.

즉, 참조 카운트가 0이 되었을 때 호출할 함수를 직접 지정할 수 있다는 것이다.

// shared_ptr

void lock(std::mutex* pm)
{
	pm->lock();
}
 
void unlock(std::mutex* pm)
{
	pm->unlock();
}
 
class Lock {
private:
	std::tr1::shared_ptr<std::mutex> mutexPtr;
public:
	explicit Lock(std::mutex *pm) : mutexPtr(pm, unlock) // explicit : 자동 형변환 방지
	{
		lock(mutexPtr.get());
	}
 
};

위의 예시 코드에서 하나 더 주목해야 할 점은 Lock 클래스의 소멸자를 정의하지 않았다는 것인데,

이것은 컴파일러가 자동으로 생성해주는 소멸자를 사용하겠다는 것이다.

항목 5에서 언급한 것처럼 클래스 소멸자는 자동으로 비정적 데이터 멤버의 소멸자를 호출한다.

그렇기 때문에 생성 시점에 이미 metexPtr이라는 멤버에 unlock 이라는 삭제자를 지정해줬으니,

Lock 클래스가 소멸하면서 자동으로 mutexPtr의 소멸자가 호출되면서 unlock이라는 삭제자 함수가 실행되는 것이다.

3. Deep Copy

→ 자원 관리 객체에 대한 복사 뿐만 아니라 관리되는 자원까지 함께 복사하는 방식

4. 자원 소유권 이전

→ auto_ptr 에서 보았던 방식과 마찬가지로 복사되는 새로운 대상에게로 자원의 소유권을 넘겨주고, 원본 자원 관리 객체는 NULL 처리 해버리는 방식

 

Things to Remember

  • RAII 객체의 복사는 그 객체가 관리하는 자원의 복사 문제를 함께 생각해야 하기 때문에, 그 자원을 어떻게 복사하느냐에 따라서 RAII 객체의 복사 동작이 결정된다.
  • RAII 클래스에 구현하는 일반적인 복사 동작은 복사를 금지하거나 참조 카운팅 방식을 사용하는 것이다.

관련글 더보기

댓글 영역