C++ 스마트 포인터 완전 정리: unique_ptr, shared_ptr, weak_ptr 그리고 면접에서 원하는 답변까지

//
//

🚀 들어가며

C++ 스마트 포인터에 대해 살펴봅니다.

C++에서 메모리 관리는 항상 중요한 주제입니다.

C++는 사용자(개발자)에게 메모리 관리 책임을 맡기는 대표적인 프로그래밍 언어입니다.

원시 포인터를 직접 다룰 때는 아래와 같이 메모리를 할당하고 해제할 수 있습니다.

MyClass* obj = new MyClass();
delete obj;

하지만 이 방식은 여러 문제를 가지고 있습니다.

  • 실수로 인한 delete 누락 → 메모리 누수로 이어집니다.
  • 예외 발생 시 delete 호출 안됨
  • 코드 흐름이 복잡해 어디에서 delete를 호출해야 할 지 알 수 없는 경우가 많음

이런 여러 문제를 해결하기 위해 등장한 것이 바로
👉 스마트 포인터(smart pointer)입니다.


🧠 스마트 포인터의 본질

C++ 스마트 포인터의 구조와 차이를 한 눈에 이해할 수 있도록 정리해보았습니다.

C++ 스마트 포인터 구조 설명 unique_ptr shared_ptr weak_ptr 비교와 사용 기준
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;
};

 

문제의 상황

위의 코드는 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++ 메모리 관리의 핵심은 대부분 정리됩니다.

 

👉 게임 엔진 구조를 더 깊이 이해하고 싶다면

아래 강의를 통해 직접 구현해보는 것을 추천드립니다.

C++로 만드는 게임 엔진 프레임워크 강의

👉 C++로 만드는 게임 엔진 프레임워크 강의 바로가기

//
   

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다