본문 바로가기
프로그래밍/C\C++

[Effective C++ 정리 #13] RAII란 무엇인가? RAII로 자원 관리 방법

by 허구의 2025. 6. 23.
728x90

이 글은 『Effective C++』를 읽고 개인적으로 공부한 내용을 정리한 기록입니다.

저는 컴퓨터공학을 전공하지 않았으며, 프로그래밍을 공부하는 과정에서의 이해와 생각을 정리하기 위해 글을 작성하고 있습니다.

따라서 내용 중 일부에 오류나 부정확한 설명이 있을 수 있으며, 피드백은 언제든지 환영합니다. 확인 후 수정하도록 하겠습니다.

전문적인 해설이 아닌 개인적 시선에서의 정리임을 참고하고 읽어주시면 감사하겠습니다.

[Effective C++ 정리 #13] 객체를 이용해 자원을 관리하라

C++에서 자원을 안전하게 관리하는 가장 강력한 방법은 객체를 통해 관리하는 것입니다. 메모리, 파일 핸들, 네트워크 소켓, 뮤텍스, 데이터베이스 연결 등 다양한 자원들은 **획득과 동시에 객체에 위임하고, 소멸자가 이를 해제하도록 하는 것**이 C++ 스타일 자원 관리의 핵심입니다.


해당 #13 아이템에서는 자원을 직접 관리하는 코드를 작성할 때 발생하는 문제점들과, 그 해결책으로 등장하는 RAII (Resource Acquisition Is Initialization) 패턴의 강력함을 알아봅니다.


자원을 수동으로 관리할 때의 위험

다음은 투자 관리 라이브러리에서 제공하는 팩토리 함수를 호출하여 동적으로 Investment 객체를 생성하는 예제입니다:

class Investment { ... };

Investment* createInvestment();  // 호출자가 반드시 delete 해야 함

void f() {
    Investment* pInv = createInvestment();

    ... // pInv 사용

    delete pInv;
}

 

객체를 생성하고, 사용한 후 delete를 호출하고 있으니 표면적으로는 문제가 없어 보입니다. 그러나 이 코드는 다음과 같은 다양한 상황에서 쉽게 문제가 발생할 수 있습니다:

  • 중간에 return이 호출되어 delete에 도달하지 않는 경우
  • breakgoto를 사용해 조기 종료되는 경우
  • 중간 코드에서 예외가 발생해 delete에 도달하지 못하는 경우

특히 예외가 등장하는 Modern C++ 환경에서는 위와 같은 코드가 **자원 누수(resource leak)**를 일으킬 가능성이 매우 높습니다.


RAII의 기본 아이디어: 생성자에서 획득하고 소멸자에서 반환하라

이러한 위험을 제거하기 위해 C++는 **소멸자의 자동 호출**이라는 강력한 메커니즘을 제공합니다. 이것이 바로 RAII (Resource Acquisition Is Initialization)의 핵심입니다.

 

자원을 획득하자마자 이를 관리하는 객체를 생성하고, 스코프를 벗어날 때 소멸자를 통해 자원을 자동으로 반납하도록 만드는 것입니다.

 

#include <memory>

void f() {
    std::auto_ptr<Investment> pInv(createInvestment());

    ... // pInv 사용

}  // 스코프를 벗어나면 pInv의 소멸자가 호출되어 자동으로 delete

 

여기서 등장한 std::auto_ptr는 초기 C++ 표준 라이브러리에서 제공하던 스마트 포인터로, 이 객체는 생성자에서 포인터를 받고, 소멸자에서 delete를 호출해 자원을 자동으로 반환합니다.

 

RAII의 두 가지 핵심 요소

RAII를 적용할 때 반드시 지켜야 하는 두 가지 원칙이 있습니다:

  1. 자원을 획득하자마자 즉시 관리 객체에 소유권을 넘긴다.
  2. 소멸자가 자원을 해제하도록 보장한다.

이 원칙을 지키면 조기 종료, 예외, 복잡한 경로를 타더라도 자원 누수가 발생하지 않습니다. 소멸자는 스코프를 벗어나는 순간 항상 호출되기 때문입니다.

 

auto_ptr의 치명적 단점 — 복사 시 소유권 이전

auto_ptr는 C++98에서 등장했지만 다음과 같은 특이한 복사 동작을 합니다:

std::auto_ptr<Investment> pInv1(createInvestment());
std::auto_ptr<Investment> pInv2(pInv1);  // pInv2가 소유권 획득, pInv1은 null

 

복사 생성자나 대입 연산자를 호출하면 기존 포인터(pInv1)는 null이 되고 새 포인터(pInv2)가 소유권을 독점합니다.

따라서:

  • STL 컨테이너에 auto_ptr를 담을 수 없습니다.
  • 복사가 아닌 이동(transfer semantics)처럼 동작합니다.

이로 인해 auto_ptr는 C++11 이후 폐기되었고, 이후 등장하는 unique_ptrshared_ptr가 대체하게 됩니다.


shared_ptr: 복사도 자연스럽고 STL과 호환

std::tr1::shared_ptr (C++11 이후 std::shared_ptr)는 참조 카운팅 기반의 스마트 포인터입니다.

#include <memory>

void f() {
    std::tr1::shared_ptr<Investment> pInv1(createInvestment());
    std::tr1::shared_ptr<Investment> pInv2(pInv1);  // 복사해도 소유권 공유

    ...  // pInv1과 pInv2가 모두 유효
}  // 마지막 포인터가 소멸될 때 자원 해제

 

shared_ptr는 다음과 같은 장점이 있습니다:

  • 복사해도 소유권을 공유함 (참조 카운트 증가)
  • STL 컨테이너에 사용 가능
  • 다중 소유권 모델링 가능

단, 순환 참조(cyclic reference)가 발생할 경우 자원이 해제되지 않는 문제가 발생할 수 있습니다.


주의: 배열은 관리할 수 없다

auto_ptrshared_ptrdelete를 호출하지만, delete[]는 호출하지 않습니다. 즉, 동적 배열에 사용하면 문제가 발생합니다:

std::auto_ptr<std::string> aps(new std::string[10]); // 잘못된 사용
std::tr1::shared_ptr<int> spi(new int[1024]);        // 역시 잘못된 사용

 

이 문제를 해결하려면 boost::scoped_arrayboost::shared_array 같은 별도 라이브러리를 사용해야 합니다.

 

따라서 본 아이템의 핵심은 프로그램 어디에서도 delete를 직접 호출하는 코드가 있어서는 안 된다고 합니다!!


핵심 요약

  • 생성자에서 자원을 획득하고, 소멸자에서 반환하게 하는 RAII 객체를 사용하라.
  • auto_ptr는 이동성만 제공하고 복사가 비정상적이므로 shared_ptr가 일반적으로 더 유용하다.

감사합니다.

728x90