이 글은 『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
에 도달하지 않는 경우 break
나goto
를 사용해 조기 종료되는 경우- 중간 코드에서 예외가 발생해
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를 적용할 때 반드시 지켜야 하는 두 가지 원칙이 있습니다:
- 자원을 획득하자마자 즉시 관리 객체에 소유권을 넘긴다.
- 소멸자가 자원을 해제하도록 보장한다.
이 원칙을 지키면 조기 종료, 예외, 복잡한 경로를 타더라도 자원 누수가 발생하지 않습니다. 소멸자는 스코프를 벗어나는 순간 항상 호출되기 때문입니다.
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_ptr
와 shared_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_ptr
와 shared_ptr
는 delete
를 호출하지만, delete[]
는 호출하지 않습니다. 즉, 동적 배열에 사용하면 문제가 발생합니다:
std::auto_ptr<std::string> aps(new std::string[10]); // 잘못된 사용
std::tr1::shared_ptr<int> spi(new int[1024]); // 역시 잘못된 사용
이 문제를 해결하려면 boost::scoped_array
나 boost::shared_array
같은 별도 라이브러리를 사용해야 합니다.
따라서 본 아이템의 핵심은 프로그램 어디에서도 delete
를 직접 호출하는 코드가 있어서는 안 된다고 합니다!!
핵심 요약
- 생성자에서 자원을 획득하고, 소멸자에서 반환하게 하는 RAII 객체를 사용하라.
auto_ptr
는 이동성만 제공하고 복사가 비정상적이므로shared_ptr
가 일반적으로 더 유용하다.
감사합니다.
'프로그래밍 > C\C++' 카테고리의 다른 글
[Effective C++ 정리 #15] RAII의 Raw resource 접근 방법 (2) | 2025.06.25 |
---|---|
[Effective C++ 정리 #14] 리소스 관리 클래스 복사 시 유의사항 (3) | 2025.06.24 |
[Effective C++ 정리 #12] 복사 생성자와 복사 대입 연산자 직접 작성 시 유의할 점 (0) | 2025.06.22 |
C/C++ 얕은 복사 및 깊은 복사 내용 정리 (2) | 2025.06.21 |
[Effective C++ 정리 #11] operator= 시 자기 자신 대입 유의! (0) | 2025.06.21 |