Search

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

개요

RAII(Resource Acquisition Is Initialization) 패턴은 C++에서 자원 관리를 위한 핵심 기법입니다. 이전 항목에서 우리는 RAII의 기본 개념을 학습했지만, 이번에는 한 걸음 더 나아가 RAII 객체의 복사 동작에 대해 심도 있게 살펴보겠습니다. 자원 관리 클래스의 복사 동작을 어떻게 설계하느냐에 따라 프로그램의 안정성과 효율성이 크게 달라질 수 있습니다. 이 항목에서는 RAII 객체의 복사 전략과 그에 따른 트레이드오프를 탐구하며, 실제 개발 상황에서 어떻게 적용할 수 있는지 알아보겠습니다.

본문

RAII 패턴의 복사 동작 전략

RAII 객체가 복사될 때 우리는 네 가지 주요 전략 중 하나를 선택할 수 있습니다:
1.
복사 금지
2.
참조 카운팅
3.
깊은 복사
4.
소유권 이전
각 전략을 자세히 살펴보겠습니다.

1. 복사 금지

많은 경우, RAII 객체의 복사 자체가 의미가 없거나 위험할 수 있습니다. 예를 들어, 뮤텍스를 관리하는 RAII 객체의 경우 복사가 허용되면 동기화 문제가 발생할 수 있습니다.
class Lock : private Uncopyable { public: explicit Lock(Mutex* pm) : mutexPtr(pm) { lock(mutexPtr); } ~Lock() { unlock(mutexPtr); } private: Mutex* mutexPtr; };
C++
복사
이 예제에서 Lock 클래스는 Uncopyable을 상속받아 복사를 금지합니다. 이는 Lock 객체가 실수로 복사되는 것을 컴파일 시점에 방지합니다.

2. 참조 카운팅

자원을 공유하고 마지막 사용자가 자원을 해제하도록 하고 싶다면, 참조 카운팅을 사용할 수 있습니다. C++의 std::shared_ptr이 이 전략을 구현한 대표적인 예입니다.
class Lock { public: explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) { lock(mutexPtr.get()); } private: std::shared_ptr<Mutex> mutexPtr; };
C++
복사
여기서 shared_ptr의 두 번째 인자로 unlock 함수를 전달하여 커스텀 삭제자를 지정했습니다. 이렇게 하면 참조 카운트가 0이 될 때 자동으로 unlock이 호출됩니다.

3. 깊은 복사

일부 경우에는 자원 자체를 복사하는 것이 적절할 수 있습니다. 예를 들어, 문자열을 관리하는 RAII 객체의 경우 깊은 복사가 필요할 수 있습니다.
class String { public: String(const char* str) : data(new char[strlen(str) + 1]) { strcpy(data, str); } String(const String& rhs) : data(new char[strlen(rhs.data) + 1]) { strcpy(data, rhs.data); // 깊은 복사 수행 } ~String() { delete[] data; } private: char* data; };
C++
복사
String 클래스는 복사 생성자에서 깊은 복사를 수행하여 각 객체가 자신만의 문자열 복사본을 갖도록 합니다.

4. 소유권 이전

때로는 자원의 소유권을 한 객체에서 다른 객체로 이전하는 것이 유용할 수 있습니다. std::unique_ptr이 이 전략을 사용합니다.
template<class T> class UniqueResource { T* resource; public: UniqueResource(T* res) : resource(res) {} UniqueResource(UniqueResource&& other) noexcept : resource(other.resource) { other.resource = nullptr; // 소유권 이전 } ~UniqueResource() { delete resource; } UniqueResource(const UniqueResource&) = delete; // 복사 생성자 삭제 UniqueResource& operator=(const UniqueResource&) = delete; // 복사 대입 연산자 삭제 };
C++
복사
UniqueResource 클래스는 복사를 금지하고 이동 생성자만을 제공하여 소유권 이전을 구현합니다.

임계 영역과 RAII

RAII는 임계 영역(critical section) 관리에 특히 유용합니다. 임계 영역은 여러 스레드가 동시에 접근하면 안 되는 코드 부분을 말합니다. RAII를 사용하면 뮤텍스의 잠금과 해제를 자동화할 수 있습니다.
void criticalOperation() { Lock lock(&mutex); // 생성자에서 뮤텍스 잠금 // 임계 영역 코드 } // lock의 소멸자에서 자동으로 뮤텍스 해제
C++
복사
이렇게 하면 예외가 발생하더라도 뮤텍스가 안전하게 해제되어 데드락을 방지할 수 있습니다.

개인적 견해

RAII와 그 복사 전략은 다양한 종류의 자원 관리에 적용할 수 있다는 점이 인상적입니다. 힙 메모리뿐만 아니라 파일 디스크립터, 데이터베이스 연결, 그래픽 자원 등 여러 시스템 리소스를 안전하게 관리할 수 있습니다. 특히 shared_ptr의 소멸 과정에 대한 이해는 참조 카운팅 전략을 효과적으로 활용하는 데 큰 도움이 됩니다.
실제 프로젝트에서 이러한 RAII 패턴을 적용할 때, 각 자원의 특성과 사용 패턴을 고려하여 적절한 복사 전략을 선택해야 할 것 같습니다. 예를 들어, 공유 자원을 다루는 멀티스레드 환경에서는 참조 카운팅이 유용할 수 있고, 유니크한 시스템 리소스를 다룰 때는 소유권 이전이나 복사 금지 전략이 적합할 수 있겠습니다.

결론

RAII 객체의 복사 동작을 신중하게 설계하는 것은 안전하고 효율적인 자원 관리를 위해 필수적입니다. 복사 금지, 참조 카운팅, 깊은 복사, 소유권 이전 등 다양한 전략 중에서 상황에 가장 적합한 것을 선택함으로써 자원 누수를 방지하고, 예외 안전성을 높이며, 동시성 문제를 효과적으로 다룰 수 있습니다.