🚀 들어가며
C++ 스마트 포인터에 대해 살펴봅니다.
C++에서 메모리 관리는 항상 중요한 주제입니다.
C++는 사용자(개발자)에게 메모리 관리 책임을 맡기는 대표적인 프로그래밍 언어입니다.
원시 포인터를 직접 다룰 때는 아래와 같이 메모리를 할당하고 해제할 수 있습니다.
MyClass* obj = new MyClass(); delete obj;
하지만 이 방식은 여러 문제를 가지고 있습니다.
- 실수로 인한 delete 누락 → 메모리 누수로 이어집니다.
- 예외 발생 시 delete 호출 안됨
- 코드 흐름이 복잡해 어디에서 delete를 호출해야 할 지 알 수 없는 경우가 많음
이런 여러 문제를 해결하기 위해 등장한 것이 바로
👉 스마트 포인터(smart pointer)입니다.
🧠 스마트 포인터의 본질
C++ 스마트 포인터의 구조와 차이를 한 눈에 이해할 수 있도록 정리해보았습니다.

unique_ptr은 단일 소유권, shared_ptr은 공유 소유권, weak_ptr은 비소유 참조를 의미합니다.
스마트 포인터는 단순히 원시 포인터를 대체하는 도구가 아닙니다.
👉 객체의 생명주기를 자동으로 관리하는 도구입니다.
C++에서는 상황에 맞게 사용할 수 있도록 3가지 스마트 포인터를 제공합니다.
이 세 가지를 제대로 이해하려면 하나의 질문으로 정리할 수 있습니다.
- “이 객체를 누가, 언제 삭제할 것인가?”
이 질문에 대한 답에 따라 어떤 스마트 포인터를 선택해야 하는지가 결정됩니다.
🎯 std::unique_ptr
C++에서 제공하는 스마트 포인터 중 가장 기본이 되는 타입입니다.
👉 단일 소유권(Single Ownership)을 가지는 스마트 포인터입니다.
✔ 특징
- 하나의 객체는 하나의 unique_ptr만 소유할 수 있습니다.
- 복사가 불가능합니다.
- 소유권 이전은 이동(std::move)을 통해서만 가능합니다.
- 오버헤드가 거의 없는 가장 가벼운 스마트 포인터입니다.
✔ 기본적인 사용 방법
class Actor
{
public:
Actor() { }
};
std::unique_ptr<Actor> newActor = std::make_unique<Actor>();
과거에는 아래와 같이 사용하기도 했지만,
std::unique_ptr<Actor> newActor = std::unique_ptr<Actor>(new Actor());
👉 현재(c++ 14이후)는 std::make_unique 사용이 권장됩니다.
✔ 언제 사용하는가?
다음과 같은 상황이라면 unique_ptr을 사용하는 것이 가장 자연스럽습니다.
- 객체의 소유자가 명확할 때
- 하나의 시스템 또는 객체에서만 관리할 때
- 객체의 생명주기가 특정 스코프에 종속될 때
👉 예:
- 게임 오브젝트 내부에서만 사용하는 컴포넌트
- 씬 내부에서만 사용하는 객체
- 리소스 매니저 내부에서 생성/삭제가 명확한 경우
✔ 핵심
👉 “이 객체는 내가 책임지고 삭제한다”
실제로는 대부분의 경우 unique_ptr이 기본 선택이 됩니다.
🎯 std::shared_ptr
shared_ptr은 unique_ptr과 달리 여러 객체가 하나의 객체를 함께 소유할 수 있는 구조입니다.
✔ 특징
shared_ptr의 특징을 다음과 같이 정리해볼 수 있습니다.
- 참조 카운팅 기반으로 동작합니다.
- shared_ptr이 복사될 때마다 참조 카운트가 증가합니다.
- 마지막 shared_ptr이 사라질 때 객체가 삭제됩니다. 즉, 참조 카운트가 0이 되는 순간 메모리에서 해제됩니다.
✔ 기본적인 사용 방법
class Actor
{
public:
Actor() { }
};
std::shared_ptr<Actor> actor = std::make_shared<Actor>();
unique_ptr과 마찬가지로 과거에는 아래와 같이 사용하기도 했지만,
std::shared_ptr<Actor> newActor = std::shared_ptr<Actor>(new Actor());
👉 현재(c++ 14이후)는 std::make_shared 사용이 권장됩니다.
✔ 언제 사용하는가?
shared_ptr을 사용하는 상황은 생각보다 제한적입니다.
단순히 “여러 곳에서 사용한다”는 이유로 사용하는 것이 아니라,
👉 객체의 생명주기를 한 곳에서 관리할 수 없는 경우에 사용합니다.
예를 들어:
- 이벤트 시스템
- 비동기 작업
- 리소스 캐시
- 그래프 구조
이런 구조에서는
- 누가 마지막 사용자일지 알 수 없습니다.
- 언제 객체가 더 이상 필요 없어질지 판단하기 어렵습니다.
이런 상황에서 shared_ptr이 필요합니다.
✔ 핵심
shared_ptr은 객체의 소멸 시점을 명확히 알 수 없는 경우에 사용합니다.
이 문장이 매우 중요합니다.
실제 면접에서 스마트 포인터 관련 질문 중 아래와 비슷한 질문이 자주 등장합니다.
- 언제 shared_ptr을 사용하나요?
이 질문에 대한 대부분의 면접관들이 원하는 답변은 위의 설명과 같습니다.
- “객체의 소멸 시점을 정확하게 판단할 수 없을 때 shared_ptr을 사용합니다”
물론, “소유권” 즉, 스마트 포인터에 대한 일반적인 내용에 대한 답변을 한 뒤에 앞의 내용을 보강해주는 것이 더 좋습니다.
⚠️ shared_ptr의 문제: 순환 참조
객체의 소유권을 공유할 수도 있고,
객체의 소멸 시점을 정확하게 판단할 수 없을 때에도 메모리 관리를 알아서 해주기 때문에
shared_ptr은 편리해 보이지만, 잘못 사용하면 오히려 메모리 관리가 더 어려워질 수 있습니다.
shared_ptr을 사용할 때 반드시 알아야 하는 문제가 있습니다.
바로 “순환 참조”입니다.
struct B;
struct A
{
std::shared_ptr<B> b;
};
struct B
{
std::shared_ptr<A>
문제의 상황
위의 코드는 A객체가 B객체를 참조하고, B객체가 A객체를 참조합니다.
즉, 서로 참조하는 상황이 발생했습니다.
A → B B → A
결과
이렇게 서로 참조하는 상황에 shared_ptr은 심각한 문제를 야기합니다.
객체의 소유권을 공유할 수 있기 때문에 서로 참조를 하게 되면, 참조 카운트가 0이 되지 않는 상태가 발생합니다.
이런 상황이 발생하면 영원히 소멸되지 않는 메모리가 발생하게 됩니다.
참조 카운트가 0이 되지 않음 → 메모리 해제 안됨
이것이 바로 순환 참조 (Reference Cycle) 입니다.
shared_ptr만으로는 모든 상황에서 안전한 메모리 관리를 보장할 수 없습니다.
🧩 std::weak_ptr
이 순환 참조 문제를 해결하기 위해 등장한 것이 weak_ptr입니다.
✔ 특징
weak_ptr의 특징을 다음과 같이 정리해볼 수 있습니다.
- 객체를 소유하지 않습니다.
- 참조 카운트를 증가시키지 않습니다.
- 객체의 생명주기에 영향을 주지 않습니다.
✔ 사용 방법
weak_ptr은 그 자체로는 생성을 할 수 없고, 다른 shared_ptr을 가리킬 수 있게 설계되어 있습니다.
class Actor
{
public:
Actor() { }
};
// shared_ptr 타입의 액터 생성.
std::shared_ptr<Actor> newActor = std::make_shared<Actor>();
// newActor를 가리키는 weak_ptr 생성.
std::weak_ptr<Actor> otherActor = newActor;
이렇게 shared_ptr을 가리킬 수 있게 초기화된 weak_ptr은 아래 코드와 같이 접근할 수 있습니다.
if (std::shared_ptr<Actor> actor = otherActor.lock())
{
// 안전하게 접근 가능.
}
weak_ptr이 shared_ptr을 가리킬 때 참조 카운트를 증가시키지 않습니다.
weak_ptr은 shared_ptr이 관리하는 객체를 직접 참조하지 않고, 간접 참조합니다.
즉, weak_ptr은 shared_ptr이 관리하는 control block을 참조합니다.
따라서 weak_ptr을 통해 shared_ptr에 접근할 때는 lock()이라는 별도의 함수를 통해 shared_ptr이 관리하는 객체에 접근해야 합니다.
lock()은 shared_ptr이 관리하는 실제 객체가 메모리에 살아 있으면 해당 객체를 반환하고, 메모리에서 해제되어 소멸되면 nullptr을 반환해줍니다.
따라서 lock() 이후에 반환된 객체의 null 여부를 판단해 안전하게 해당 객체에 접근할 수 있도록 설계되어 있습니다.
✔ 언제 사용하는가?
- 앞선 예제의 상황과 같이 순환 참조를 끊을 때
- 객체를 소유하지 않고 참조만 하고 싶을 때
- Observer 패턴
✔ 핵심
weak_ptr은 어떤 객체를 사용할 때 “사용”만 하고, 해당 객체의 메모리 관리 책임은 없을 때 사용합니다.
즉, “이 객체를 살려둘 책임은 없을 때” 사용합니다.
🎯 스마트 포인터 선택 기준
지금까지 내용을 정리하면 다음과 같습니다.
- unique_ptr → 단일 소유
- shared_ptr → 공유 소유
- weak_ptr → 비소유 참조
하지만 더 중요한 기준은 이것입니다.
👉 “이 객체의 생명주기를 누가 책임지는가?”
🎤 면접 질문 정리
다시 질문으로 돌아가보겠습니다.
- “std::shared_ptr은 언제 사용하나요?”
👉 좋은 답변
- 객체의 소멸 시점을 명확히 알 수 없을 때 사용합니다.
👉 보완 답변
여러 객체가 해당 객체의 생명주기를 공유해야 하고,
누가 마지막 사용자일지 알 수 없는 경우에 사용합니다.가능하면 unique_ptr을 우선적으로 사용하고,
비소유 참조에는 weak_ptr을 사용하는 것이 바람직합니다.
🧠 마무리
스마트 포인터는 단순한 문법이 아니라 모던 C++에서 객체를 관리할 때 필수로 사용해야 하는 설계 도구라고 할 수 있습니다.
스마트 포인터를 사용할 때 필수로 확인해야 하는 질문은 이것입니다.
- “이 객체는 언제 소멸되어야 하는가?”
이 질문에 대한 답변에 따라 아래와 같이 스마트 포인터의 타입을 선택할 수 있습니다.
- 질문에 답할 수 있다면: unique_ptr
- 답할 수 없다면: shared_ptr
- 객체의 소멸을 관리하지 않고, 참조(사용)만 하는 상황이라면: weak_ptr
이 세 가지를 이해하면 C++ 메모리 관리의 핵심은 대부분 정리됩니다.