[C++] 항목 13: 자원 관리에는 객체가 그만!
카테고리: Cpp
태그: Cpp
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역
📦 3. 자원 관리
👉🏻 항목 13: 자원 관리에는 객체가 그만!
class Investment { ... }; // 최상위 클래스
// Investment 클래스 계통에 속한 객체를 동적 할당하고,
// 그 포인터를 반환한다.
// 객체의 해제는 호출자 쪽에서 직접해야 한다.
// (매개변수 생략)
Investment* createInvestment();
Investment
클래스는 상속받은 객체를 리턴하는 팩토리 함수인 createInvestment
만을 통해 객체를 만들게 되어있다고 가정해보자.
이때, createInvestment
함수로부터 객체를 받은 사람은 그 객체를 직접 해제(delete) 해야 한다.
void f() {
Investment *pInv = createInvestment(); // 팩토리 함수 호출
...
delete pInv; // 객체 해제
}
하지만 이 코드에는 자원이 누출될 수 있는 위험이 있다.
예를 들어:
...
내부에return
문이 있으면delete
가 실행되지 않음- 루프 내부에서
createInvestment()
를 호출하고, 중간에continue
나goto
가 등장하는 경우 - 예외가 발생하는 경우
이럴 경우, 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 패턴
- 자원을 확보한 후, 객체에 집어넣자. (RAII)
- 자원 관리 객체의 소멸자에서 자원을 해제하게 만들자.
- 예외, 루프 탈출, 조건 분기 등에도
delete
가 자동으로 실행된다.
🧨 직접 자원 해제를 하면 생기는 실수들
delete
를 깜빡하거나- 스마트 포인터 사용을 잊거나
- 복사/할당으로 소유권이 꼬이거나
결국엔 자원 누수!
그래서 자원 해제는 직접 하지 말고 RAII 객체(예: 스마트 포인터)에게 맡기자.
🔮 다음 항목 예고
근본적인 문제는 createInvestment()
의 반환 타입이 생 포인터(raw pointer) 라는 점이다.
이런 방식은 사람의 실수를 유도한다.
delete
를 안 하거나- 스마트 포인터로 안 감싸거나
이 문제에 대한 해결책은 항목 18에서 계속 다룬다!
🧐 정리
- 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는 RAII 객체를 사용하자.
- 일반적으로 RAII 클래스는
shared_ptr
과auto_ptr
이 있다.
shared_ptr
이 복사 시 동작이 더 직관적이라 더 좋다.
auto_ptr
은 복사 시 원본 객체를 null로 만든다. - 스마트 포인터는
delete
를 대신 호출해주므로, 예외나 분기 상황에서도 안전하다. - 배열에는
shared_ptr
을 쓰지 말자. 배열은delete []
가 필요하기 때문이다.
댓글남기기