[C++] 항목 13: 자원 관리에는 객체가 그만!

업데이트:     Updated:

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역

📦 3. 자원 관리

👉🏻 항목 13: 자원 관리에는 객체가 그만!

class Investment { ... }; // 최상위 클래스
// Investment 클래스 계통에 속한 객체를 동적 할당하고,
// 그 포인터를 반환한다.
// 객체의 해제는 호출자 쪽에서 직접해야 한다.
// (매개변수 생략)
Investment* createInvestment();

Investment 클래스는 상속받은 객체를 리턴하는 팩토리 함수인 createInvestment만을 통해 객체를 만들게 되어있다고 가정해보자.
이때, createInvestment 함수로부터 객체를 받은 사람은 그 객체를 직접 해제(delete) 해야 한다.

void f() {
	Investment *pInv = createInvestment(); // 팩토리 함수 호출
	...
	delete pInv; // 객체 해제
}

하지만 이 코드에는 자원이 누출될 수 있는 위험이 있다.

예를 들어:

  1. ... 내부에 return 문이 있으면 delete가 실행되지 않음
  2. 루프 내부에서 createInvestment()를 호출하고, 중간에 continuegoto가 등장하는 경우
  3. 예외가 발생하는 경우

이럴 경우, delete를 호출하지 못한 채 함수가 종료되고,
메모리는 물론 객체가 소유한 리소스까지 누출된다.

⛑️ 해결책: 자원 해제는 소멸자에게 맡기자

이 문제를 해결하려면, 자원의 해제를 소멸자에게 맡기고,
그 소멸자가 함수가 끝날 때 자동으로 호출되도록 하는 것이 좋다.

즉, delete는 사람이 직접 하지 말고, 스마트 포인터 같은 객체가 하게 하자!



✨ 스마트 포인터(smart pointer)의 등장

스마트 포인터는 포인터처럼 행동하지만,
자신이 소멸될 때 delete를 자동으로 호출해주는
RAII(Resource Acquisition Is Initialization) 객체이다.

1. auto_ptr (C++98/03 기준, 현재는 사용 비추천)

void f() {
	auto_ptr<Investment> pInv(createInvestment());
	...
} // 함수 종료 시, pInv의 소멸자가 실행되고 delete 자동 호출

auto_ptr은 자기 자신이 소멸될 때 가리키는 객체를 자동으로 해제한다.

그러나 한 객체에 대해 auto_ptr이 두 개 이상 존재하면 곤란하다.
그래서 auto_ptr복사 시, 원래 객체를 null로 만든다.

auto_ptr<Investment> pInv1(createInvestment());
auto_ptr<Investment> pInv2(pInv1); // pInv1은 null, pInv2만 유효

pInv1 = pInv2; // pInv2는 null, pInv1만 유효

이처럼 auto_ptr자원의 유일 소유권(unique ownership) 을 보장하려 한다.

하지만, 이 때문에 auto_ptr은 STL 컨테이너에서는 사용할 수 없다.
(STL 컨테이너는 내부적으로 복사를 요구하기 때문)


2. shared_ptr (C++11 이후 표준)

shared_ptr참조 카운팅 방식(reference-counting) 스마트 포인터다.

즉, 특정 자원을 가리키는 포인터의 수를 내부적으로 세고 있다가,
마지막 포인터가 소멸되면 자원을 자동으로 delete 한다.

void f() {
	shared_ptr<Investment> pInv(createInvestment());
	...
} // pInv가 마지막 포인터였다면, 소멸자에서 delete 자동 실행
shared_ptr<Investment> pInv1(createInvestment());
shared_ptr<Investment> pInv2(pInv1); // 둘 다 같은 객체를 가리킴

pInv1 = pInv2; // 참조 횟수 변화 없음

위 코드에선 pInv1, pInv2 둘 다 같은 자원을 공유한다.
함수가 끝나서 두 포인터가 모두 파괴되면, 참조 수가 0이 되어 객체가 자동 해제된다.

📌 가비지 컬렉션과의 차이

  • shared_ptr은 참조 카운팅 방식으로 동작한다.
  • 가비지 컬렉션처럼 자동으로 메모리를 수거해주지만,
  • 순환 참조(예: A가 B를 참조하고, B도 A를 참조하는 경우)는 해결 못 한다.



3. 배열에는 주의하자!

스마트 포인터는 기본적으로 delete 연산자를 사용한다.

하지만 배열에는 delete []를 써야 한다.
그런데 auto_ptr, shared_ptr은 이걸 처리하지 못한다.

auto_ptr<string> aps(new string[10]); // 잘못된 delete 호출!
shared_ptr<int> spi(new int[1024]);   // 잘못된 delete 호출!

즉, 배열에는 기본 스마트 포인터를 쓰면 안 된다.
필요하다면 Boost의 전용 스마트 포인터(항목 55 참고)를 쓰자.


✅ 요점 정리: RAII 패턴

  1. 자원을 확보한 후, 객체에 집어넣자. (RAII)
  2. 자원 관리 객체의 소멸자에서 자원을 해제하게 만들자.
  3. 예외, 루프 탈출, 조건 분기 등에도 delete가 자동으로 실행된다.

🧨 직접 자원 해제를 하면 생기는 실수들

  • delete를 깜빡하거나
  • 스마트 포인터 사용을 잊거나
  • 복사/할당으로 소유권이 꼬이거나

결국엔 자원 누수!

그래서 자원 해제는 직접 하지 말고 RAII 객체(예: 스마트 포인터)에게 맡기자.


🔮 다음 항목 예고

근본적인 문제는 createInvestment()의 반환 타입이 생 포인터(raw pointer) 라는 점이다.

이런 방식은 사람의 실수를 유도한다.

  • delete를 안 하거나
  • 스마트 포인터로 안 감싸거나

이 문제에 대한 해결책은 항목 18에서 계속 다룬다!


🧐 정리

  1. 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는 RAII 객체를 사용하자.
  2. 일반적으로 RAII 클래스는 shared_ptrauto_ptr이 있다.
    shared_ptr이 복사 시 동작이 더 직관적이라 더 좋다.
    auto_ptr은 복사 시 원본 객체를 null로 만든다.
  3. 스마트 포인터는 delete를 대신 호출해주므로, 예외나 분기 상황에서도 안전하다.
  4. 배열에는 shared_ptr을 쓰지 말자. 배열은 delete []가 필요하기 때문이다.

Cpp 카테고리 내 다른 글 보러가기

댓글남기기