C++ 스마트 포인터 기초 완벽 가이드 | unique_ptr·shared_ptr
이 글의 핵심
C++ delete를 깜빡해서 3일 밤새 디버깅한 경험 있나요? unique_ptr·shared_ptr·make_unique·make_shared 기초부터 실전 패턴, 자주 하는 실수, 프로덕션 체크리스트까지. 문제 시나리오로 시작하는 실전 가이드.
💡 초보자를 위한 한 줄: delete를 수동 호출하지 말고,
unique_ptr로 감싸면 스코프를 벗어날 때 자동 해제되어 누수·이중 해제를 막을 수 있습니다.
들어가며: delete를 깜빡해서 3일 밤새 디버깅한 적 있나요?
”메모리 누수인지, 이중 해제인지, Valgrind도 모르겠어요”
동적 할당한 객체를 delete를 깜빡해서 메모리 누수가 났거나, 반대로 이미 해제한 포인터를 또 delete해서 세그폴트가 난 경험이 있을 겁니다. 예외가 발생하면 delete까지 도달하지 못해 누수되기도 하고, 두 포인터가 같은 객체를 가리킬 때 둘 다 delete하면 이중 해제로 크래시합니다. 스마트 포인터는 “이 포인터가 이 객체를 소유한다”는 것을 타입으로 표현하고, 스코프를 벗어나면 자동으로 해제되게 해서 이런 버그를 원천 차단합니다. 비유하면: raw 포인터는 “열쇠를 직접 들고 다니며, 문 닫을 때 꼭 기억해서 돌려야 하는 것”이고, 스마트 포인터는 “열쇠를 지갑에 넣어 두면, 나갈 때 자동으로 문이 잠기는 것”입니다. 이 글을 읽으면:
- unique_ptr, shared_ptr의 차이와 사용 시점을 명확히 알 수 있습니다.
- make_unique, make_shared를 올바르게 사용할 수 있습니다.
- 자주 하는 실수와 해결법을 익힐 수 있습니다.
- 프로덕션에서 바로 적용할 수 있는 패턴을 배울 수 있습니다.
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
1. 문제 시나리오
시나리오 1: “예외 발생 시 delete가 호출되지 않아요”
"파일을 열고 파싱하다가 예외가 나면, 할당한 버퍼가 해제되지 않아요."
"try-catch를 어디에 넣어야 할지 모르겠어요."
상황: new로 할당한 후, 그 다음 줄에서 예외가 발생하면 delete까지 실행되지 않습니다. 수동으로 try-catch를 넣어야 하는데, 여러 경로가 있으면 모든 경로에 delete를 넣기 어렵습니다.
해결 포인트: unique_ptr을 사용하면 스코프를 벗어날 때(예외 포함) 소멸자가 자동으로 delete를 호출합니다.
시나리오 2: “두 포인터가 같은 객체를 가리키다가 이중 해제로 크래시해요”
"원본 포인터와 복사본이 같은 메모리를 가리키는데, 둘 다 delete했어요."
"어디서 이중 해제가 나는지 찾기 힘들어요."
상황: int* p = new int(42); int* q = p;처럼 두 포인터가 같은 객체를 가리킬 때, delete p; delete q;를 하면 같은 메모리를 두 번 해제하는 이중 해제(double-free)가 발생합니다.
해결 포인트: unique_ptr은 “한 시점에 하나의 소유자만” 보장하므로 이중 해제가 불가능합니다. 공유가 필요하면 shared_ptr을 사용합니다.
시나리오 3: “함수 반환 시 누가 delete할지 애매해요”
"팩토리 함수가 new로 만든 객체를 반환하는데, 호출자가 delete해야 하나요?"
"반환된 포인터를 여러 곳에서 쓰다가, 어디서 해제할지 모르겠어요."
상황: Widget* createWidget()이 new Widget()을 반환할 때, 호출자가 delete를 잊으면 누수되고, 여러 곳에서 delete하면 이중 해제가 됩니다.
해결 포인트: std::unique_ptr<Widget> createWidget()으로 반환하면 소유권이 명확해지고, 호출자가 받은 unique_ptr이 스코프를 벗어날 때 자동 해제됩니다.
시나리오 4: “여러 스레드가 같은 객체를 쓰는데, 언제 해제해야 할지 모르겠어요”
"스레드 A가 만든 객체를 스레드 B가 사용하는데, B가 끝난 뒤 A가 먼저 종료되면 use-after-free예요."
상황: 스레드 간에 객체 수명을 공유할 때, “마지막으로 사용하는 쪽”이 해제해야 하는데, 그 시점을 알기 어렵습니다.
해결 포인트: shared_ptr로 공유하면 참조 카운팅으로 “마지막 shared_ptr이 소멸될 때” 자동 해제됩니다.
시나리오 5: “C 라이브러리의 malloc/free를 C++에서 써야 해요”
"C API가 malloc으로 할당한 버퍼를 반환하는데, C++에서 free를 언제 호출해야 할지 헷갈려요."
상황: char* buf = (char*)malloc(1024);로 받은 버퍼를 C++ 코드에서 사용할 때, 예외나 early return 시 free를 깜빡하기 쉽습니다.
해결 포인트: unique_ptr에 커스텀 삭제자로 free를 지정하면 RAII로 안전하게 해제할 수 있습니다.
2. 스마트 포인터란?
RAII (Resource Acquisition Is Initialization)
스마트 포인터는 RAII 원칙을 따릅니다:
- 생성 시: 리소스 획득 (메모리 할당)
- 소멸 시: 리소스 해제 (메모리 해제) 스코프를 벗어나면(return, 예외, 블록 종료) 소멸자가 자동으로 호출되므로, 수동 delete가 필요 없습니다.
unique_ptr vs shared_ptr 한눈에
flowchart TB
subgraph U[unique_ptr]
U1["독점 소유권"]
U2["복사 불가, 이동만 가능"]
U3["오버헤드 없음 (8 bytes)"]
end
subgraph S[shared_ptr]
S1["공유 소유권"]
S2["참조 카운팅"]
S3["제어 블록 오버헤드 (16 bytes)"]
end
U --> |"공유 필요 시"| S
선택 기준 요약
| 질문 | 답변 | 사용할 타입 |
|---|---|---|
| 여러 곳에서 소유해야 하나? | No | unique_ptr |
| 여러 곳에서 소유해야 하나? | Yes | shared_ptr |
| 기본 선택은? | — | unique_ptr (90% 경우) |
3. unique_ptr 완전 가이드
unique_ptr의 특징
- 독점 소유: 한 시점에 하나의
unique_ptr만 객체를 소유 - 복사 불가: 복사 생성자·복사 대입 연산자 삭제됨
- 이동 가능:
std::move로 소유권 이전 - 오버헤드 없음: raw 포인터와 동일한 크기 (64비트에서 8 bytes)
기본 생성과 사용
#include <memory>
#include <iostream>
int main() {
// ✅ 권장: make_unique 사용
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
// ❌ 구식: new 직접 사용 (예외 안전성 문제 가능)
// std::unique_ptr<int> ptr2(new int(42));
// 역참조
std::cout << *ptr1 << std::endl; // 42
// raw 포인터 얻기 (C API 연동 시)
int* raw = ptr1.get();
// 주의: raw에 delete 하지 말 것!
// 유효성 검사
if (ptr1) {
std::cout << "ptr1 is valid\n";
}
return 0;
} // ptr1 소멸 → 자동 delete
코드 설명:
make_unique<int>(42): 힙에 int를 할당하고 unique_ptr로 감싸 반환합니다.*ptr1: 역참조로 값에 접근합니다.ptr1.get(): raw 포인터가 꼭 필요할 때만 사용합니다. 이 포인터에 delete를 하거나, 다른 스마트 포인터에 넘기면 안 됩니다.- 스코프 종료 시 소멸자가
delete를 호출합니다.
배열 지원
// 배열 생성
std::unique_ptr<int[]> arr = std::make_unique<int[]>(100);
arr[0] = 10;
arr[99] = 20;
// 또는 std::vector 사용 권장 (대부분의 경우)
std::vector<int> vec(100);
소유권 이전 (이동)
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
// ❌ 복사 불가
// std::unique_ptr<int> ptr2 = ptr1; // 컴파일 에러!
// ✅ 이동
std::unique_ptr<int> ptr2 = std::move(ptr1);
// 이동 후: ptr1은 nullptr, ptr2가 소유
assert(!ptr1);
assert(ptr2 && *ptr2 == 42);
함수 인자로 전달
소유권을 함수에 넘기는 경우
void takeOwnership(std::unique_ptr<int> ptr) {
std::cout << *ptr << std::endl;
// 함수 종료 시 ptr 소멸 → 자동 delete
}
int main() {
auto ptr = std::make_unique<int>(42);
takeOwnership(std::move(ptr)); // 소유권 이전
// ptr은 이제 nullptr
return 0;
}
소유권 유지, 읽기만 하는 경우
void useValue(const std::unique_ptr<int>& ptr) {
if (ptr) {
std::cout << *ptr << std::endl;
}
}
int main() {
auto ptr = std::make_unique<int>(42);
useValue(ptr); // 소유권 유지
std::cout << *ptr << std::endl; // 여전히 유효
return 0;
}
선택 가이드: “함수가 객체를 소유해야 하나?” → Yes: std::unique_ptr<T> 값으로 받고 std::move로 넘김. No: const std::unique_ptr<T>& 또는 T*/T&로 받음.
raw 포인터/참조로 받기 (non-owning)
void process(int* p) {
if (p) std::cout << *p << std::endl;
}
int main() {
auto ptr = std::make_unique<int>(42);
process(ptr.get()); // 소유권은 ptr에 유지
return 0;
}
함수에서 반환
std::unique_ptr<int> createValue() {
return std::make_unique<int>(42);
// RVO 또는 이동으로 반환
}
int main() {
auto ptr = createValue();
std::cout << *ptr << std::endl;
return 0;
}
커스텀 삭제자
#include <cstdlib>
#include <memory>
// C API 연동: malloc/free
void useCBuffer() {
auto buf = std::unique_ptr<char, decltype(&std::free)>(
(char*)std::malloc(1024),
&std::free
);
if (!buf) {
throw std::bad_alloc();
}
// buf 사용...
} // 소멸 시 free 자동 호출
파일 핸들 예시:
#include <cstdio>
#include <memory>
struct FileDeleter {
void operator()(FILE* fp) const {
if (fp) {
std::fclose(fp);
}
}
};
void readFile(const char* path) {
std::unique_ptr<FILE, FileDeleter> file(std::fopen(path, "r"));
if (!file) {
throw std::runtime_error("Cannot open file");
}
// 파일 읽기...
} // fclose 자동 호출
4. shared_ptr 완전 가이드
shared_ptr의 특징
- 공유 소유: 여러
shared_ptr이 동일한 객체를 소유 - 참조 카운팅: 내부적으로 참조 횟수 추적
- 자동 해제: 마지막
shared_ptr이 소멸될 때 객체 해제 - 스레드 안전: 참조 카운트 증감은 원자적 연산
기본 생성과 사용
#include <memory>
#include <iostream>
int main() {
// ✅ 권장: make_shared
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
// 복사 (참조 카운트 증가)
std::shared_ptr<int> ptr2 = ptr1;
std::shared_ptr<int> ptr3 = ptr1;
std::cout << "use_count: " << ptr1.use_count() << std::endl; // 3
ptr2.reset(); // ptr2만 해제, 카운트 2
std::cout << "use_count: " << ptr1.use_count() << std::endl; // 2
ptr3.reset(); // ptr3만 해제, 카운트 1
ptr1.reset(); // ptr1 해제, 카운트 0 → 객체 delete
return 0;
}
코드 설명:
ptr2 = ptr1: 복사 시 같은 제어 블록을 가리키며 참조 카운트가 증가합니다.ptr2.reset(): 해당 shared_ptr만 해제하고, 카운트가 1 감소합니다.- 마지막 shared_ptr이 reset되거나 소멸될 때 참조 카운트가 0이 되어 객체가 delete됩니다.
참조 카운팅 시각화
sequenceDiagram
participant M as main
participant P1 as ptr1
participant P2 as ptr2
participant P3 as ptr3
participant OBJ as Heap Object
M->>P1: make_shared (count=1)
P1->>OBJ: 생성
M->>P2: ptr2 = ptr1 (count=2)
M->>P3: ptr3 = ptr1 (count=3)
M->>P2: reset() (count=2)
M->>P3: reset() (count=1)
M->>P1: reset() (count=0)
P1->>OBJ: delete
shared_ptr 내부 구조
graph LR
subgraph Stack[스택]
P1[ptr1]
P2[ptr2]
end
subgraph Heap[힙]
CB["제어 블록br/ref_count: 2br/weak_count: 0"]
OBJ["객체br/value: 42"]
end
P1 --> CB
P1 --> OBJ
P2 --> CB
P2 --> OBJ
shared_ptr 사용 사례
사례 1: 그래프/트리 노드
struct Node {
int value;
std::vector<std::shared_ptr<Node>> children;
Node(int v) : value(v) {}
};
int main() {
auto root = std::make_shared<Node>(1);
root->children.push_back(std::make_shared<Node>(2));
root->children.push_back(std::make_shared<Node>(3));
// root 소멸 시 자식들도 함께 해제
return 0;
}
사례 2: 캐시
#include <map>
#include <memory>
#include <string>
class ResourceCache {
std::map<std::string, std::shared_ptr<Resource>> cache_;
public:
std::shared_ptr<Resource> get(const std::string& key) {
auto it = cache_.find(key);
if (it != cache_.end()) {
return it->second; // 참조 카운트 증가
}
auto res = std::make_shared<Resource>(key);
cache_[key] = res;
return res;
}
};
사례 3: 스레드 간 객체 공유
#include <thread>
#include <memory>
void runInThread(std::shared_ptr<Config> config) {
// config는 스레드가 끝날 때까지 유효
doWork(config);
}
int main() {
auto config = std::make_shared<Config>();
std::thread t(runInThread, config);
t.join();
return 0;
}
5. make_unique와 make_shared
왜 make_*를 써야 하나?
| 방식 | 할당 횟수 | 예외 안전성 |
|---|---|---|
make_unique<T>(args...) | 1회 | ✅ 안전 |
unique_ptr<T>(new T(args...)) | 1회 | ✅ 안전 (C++17) |
make_shared<T>(args...) | 1회 (객체+제어블록) | ✅ 안전 |
shared_ptr<T>(new T(args...)) | 2회 | ⚠️ 예외 시 누수 가능 |
make_unique 사용법
// 단일 객체
auto p1 = std::make_unique<int>(42);
auto p2 = std::make_unique<std::string>("hello");
// 생성자 인자 여러 개
struct Widget {
Widget(int a, int b) {}
};
auto p3 = std::make_unique<Widget>(1, 2);
// 배열 (C++14)
auto arr = std::make_unique<int[]>(10);
arr[0] = 1;
make_shared 사용법
// 단일 객체
auto p1 = std::make_shared<int>(42);
// 생성자 인자
auto p2 = std::make_shared<std::vector<int>>(10, 0);
// make_shared의 장점: 객체와 제어 블록을 한 번에 할당
// → 메모리 지역성 향상, 할당 1회로 감소
make_shared vs new + shared_ptr
// ✅ 권장: 할당 1회
auto p1 = std::make_shared<LargeObject>(arg1, arg2);
// ❌ 비권장: 할당 2회 (객체 + 제어 블록)
std::shared_ptr<LargeObject> p2(new LargeObject(arg1, arg2));
예외 안전성 문제 (C++17 이전):
// 위험: processWidget(shared_ptr<Widget>(new Widget), compute());
// new Widget과 shared_ptr 생성 사이에 예외 발생 시 Widget 누수
// make_shared는 원자적이므로 안전
processWidget(std::make_shared<Widget>(), compute());
6. 완전한 예제 코드
예제 1: 팩토리 패턴 (unique_ptr)
#include <memory>
#include <iostream>
class Document {
public:
virtual void save() = 0;
virtual ~Document() = default;
};
class PdfDocument : public Document {
public:
void save() override { std::cout << "Saving PDF\n"; }
};
class WordDocument : public Document {
public:
void save() override { std::cout << "Saving Word\n"; }
};
std::unique_ptr<Document> createDocument(const std::string& type) {
if (type == "pdf") return std::make_unique<PdfDocument>();
if (type == "word") return std::make_unique<WordDocument>();
return nullptr;
}
int main() {
auto doc = createDocument("pdf");
if (doc) {
doc->save();
}
return 0;
}
예제 2: Pimpl 패턴 (unique_ptr)
// widget.h
#include <memory>
class Widget {
public:
Widget();
~Widget();
void doSomething();
private:
class Impl;
std::unique_ptr<Impl> pImpl_;
};
// widget.cpp
#include "widget.h"
class Widget::Impl {
public:
void doSomethingInternal() {
// 구현 세부사항
}
};
Widget::Widget() : pImpl_(std::make_unique<Impl>()) {}
Widget::~Widget() = default; // 또는 .cpp에 정의 (Impl 완전 타입 필요)
void Widget::doSomething() {
pImpl_->doSomethingInternal();
}
예제 3: 다형성 컨테이너 (unique_ptr)
#include <memory>
#include <vector>
#include <iostream>
struct Shape {
virtual void draw() const = 0;
virtual ~Shape() = default;
};
struct Circle : Shape {
void draw() const override { std::cout << "Circle\n"; }
};
struct Rectangle : Shape {
void draw() const override { std::cout << "Rectangle\n"; }
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>());
shapes.push_back(std::make_unique<Rectangle>());
for (const auto& s : shapes) {
s->draw();
}
return 0;
}
예제 4: 리소스 매니저 (unique_ptr + 커스텀 삭제자)
#include <memory>
#include <iostream>
struct Resource {
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource released\n"; }
};
int main() {
{
auto res = std::make_unique<Resource>();
// 사용...
} // "Resource released" 출력
return 0;
}
예제 5: shared_ptr로 객체 공유
#include <memory>
#include <vector>
#include <iostream>
struct Session {
int id;
Session(int i) : id(i) {}
~Session() { std::cout << "Session " << id << " destroyed\n"; }
};
int main() {
auto session = std::make_shared<Session>(1);
std::vector<std::shared_ptr<Session>> handlers;
handlers.push_back(session);
handlers.push_back(session);
std::cout << "use_count: " << session.use_count() << std::endl; // 3
handlers.clear();
std::cout << "use_count: " << session.use_count() << std::endl; // 1
return 0;
} // "Session 1 destroyed" 출력
7. 자주 발생하는 에러와 해결법
에러 1: unique_ptr 복사 시도
증상: error: use of deleted function 'std::unique_ptr<T>::unique_ptr(const std::unique_ptr<T>&)'
// ❌ 잘못된 코드
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
std::unique_ptr<int> ptr2 = ptr1; // 컴파일 에러!
해결법:
// ✅ 올바른 코드: 이동 사용
std::unique_ptr<int> ptr2 = std::move(ptr1);
에러 2: get()으로 얻은 포인터에 delete 적용
증상: 이중 해제(double-free)로 크래시.
// ❌ 잘못된 코드
auto ptr = std::make_unique<int>(42);
delete ptr.get(); // 이중 해제! unique_ptr 소멸 시 또 delete
해결법: get()은 참조만 반환합니다. delete 금지. C API에 넘길 때만 사용.
에러 3: 같은 raw 포인터로 shared_ptr 여러 개 생성
증상: 이중 해제.
// ❌ 잘못된 코드
int* raw = new int(42);
std::shared_ptr<int> p1(raw);
std::shared_ptr<int> p2(raw); // 각각 별도 제어 블록 → 이중 해제!
해결법:
// ✅ 올바른 코드: make_shared 또는 한 번만 생성 후 복사
auto p1 = std::make_shared<int>(42);
std::shared_ptr<int> p2 = p1;
에러 4: this를 shared_ptr로 감싸기
증상: std::shared_ptr<MyClass>(this) 사용 시 이중 해제.
// ❌ 잘못된 코드
class Bad {
public:
std::shared_ptr<Bad> getShared() {
return std::shared_ptr<Bad>(this); // 위험!
}
};
해결법: std::enable_shared_from_this 상속 후 shared_from_this() 사용.
// ✅ 올바른 코드
class Good : public std::enable_shared_from_this<Good> {
public:
std::shared_ptr<Good> getShared() {
return shared_from_this();
}
};
// 주의: 반드시 shared_ptr로 생성된 객체에서만 호출 가능
에러 5: make_shared 대신 new + shared_ptr
증상: 할당 2회, 성능 저하.
// ❌ 비권장
std::shared_ptr<Widget> p(new Widget(1, 2, 3));
// ✅ 권장
auto p = std::make_shared<Widget>(1, 2, 3);
에러 6: unique_ptr을 복사로 함수에 전달
증상: 컴파일 에러.
// ❌ 잘못된 코드
void take(std::unique_ptr<int> p) {}
auto ptr = std::make_unique<int>(42);
take(ptr); // 복사 불가!
해결법:
// ✅ 올바른 코드
take(std::move(ptr));
에러 7: 이동 후 원본 사용
증상: nullptr 역참조 또는 정의되지 않은 동작.
// ❌ 잘못된 코드
auto ptr1 = std::make_unique<int>(42);
auto ptr2 = std::move(ptr1);
std::cout << *ptr1 << std::endl; // ptr1은 nullptr!
해결법: 이동 후 원본은 사용하지 않습니다.
8. 모범 사례와 선택 가이드
선택 플로우차트
flowchart TD
A[동적 할당 객체가 필요한가?] -->|No| B[스택 또는 참조 사용]
A -->|Yes| C[여러 곳에서 소유해야 하나?]
C -->|No| D[unique_ptr 사용]
C -->|Yes| E[순환 참조 가능성?]
E -->|No| F[shared_ptr 사용]
E -->|Yes| G[shared_ptr + weak_ptr]
모범 사례 요약
| 규칙 | 설명 |
|---|---|
| 기본은 unique_ptr | 의심스러우면 unique_ptr |
| make_unique, make_shared 사용 | new 직접 사용 지양 |
| get()으로 얻은 포인터에 delete 금지 | 소유권은 스마트 포인터에 있음 |
| this를 shared로 쓸 때 | enable_shared_from_this + shared_from_this() |
| 함수 인자 | 소유권 이전: unique_ptr 값으로 받고 std::move. 읽기만: const T& 또는 T* |
unique_ptr 사용 시기 (90%)
// 팩토리 반환
std::unique_ptr<Widget> create();
// 클래스 멤버
class Window {
std::unique_ptr<Button> closeButton_;
};
// 컨테이너
std::vector<std::unique_ptr<Shape>> shapes;
shared_ptr 사용 시기 (9%)
// 그래프/캐시
std::map<std::string, std::shared_ptr<Resource>> cache;
// 스레드 간 공유
std::thread t([config]() { use(config); });
9. 프로덕션 패턴
패턴 1: 팩토리에서 unique_ptr 반환
class DocumentFactory {
public:
static std::unique_ptr<Document> create(const std::string& path) {
return std::make_unique<PdfDocument>(path);
}
};
패턴 2: 클래스 멤버로 리소스 소유
class DatabaseConnection {
std::unique_ptr<ConnectionHandle> handle_;
std::unique_ptr<StatementCache> cache_;
public:
DatabaseConnection() :
handle_(std::make_unique<ConnectionHandle>()),
cache_(std::make_unique<StatementCache>()) {}
// 소멸자에서 자동 해제
};
패턴 3: 스레드 간 객체 공유 (shared_ptr)
void runAsync(std::shared_ptr<Config> config) {
std::thread([config]() {
process(config); // config는 스레드 종료까지 유효
}).detach();
}
패턴 4: Pimpl + unique_ptr (ABI 안정성)
헤더에 구현 세부사항을 숨기고, unique_ptr<Impl>만 두면 구현 변경 시 바이너리 호환을 유지할 수 있습니다.
패턴 5: C API 연동 (커스텀 삭제자)
auto buf = std::unique_ptr<char, decltype(&std::free)>(
(char*)std::malloc(size), &std::free);
패턴 6: enable_shared_from_this (비동기 콜백)
class AsyncService : public std::enable_shared_from_this<AsyncService> {
public:
void startAsync() {
asyncCall([self = shared_from_this()]() {
self->onComplete();
});
}
};
10. 성능 비교와 체크리스트
크기 및 오버헤드
| 타입 | 크기 (64비트) | 할당 | 비고 |
|---|---|---|---|
unique_ptr<T> | 8 bytes | 0 (객체만) | raw 포인터와 동일 |
shared_ptr<T> | 16 bytes | 제어 블록 1회 | 참조 카운팅 |
| raw 포인터 | 8 bytes | 0 | 수동 관리 |
make_shared vs new + shared_ptr
// make_shared: 객체 + 제어 블록 한 번에 할당
auto p1 = std::make_shared<LargeObject>(args...);
// new + shared_ptr: 할당 2회
std::shared_ptr<LargeObject> p2(new LargeObject(args...));
프로덕션 체크리스트
- 기본은
unique_ptr, 공유 필요 시에만shared_ptr -
make_unique,make_shared사용 (new 직접 사용 지양) -
get()으로 얻은 포인터에 delete 금지 -
this를 shared로 쓸 때는enable_shared_from_this+shared_from_this() - 양방향 참조 시 한쪽은
weak_ptr(순환 참조 방지) - 성능이 중요한 경로에서는
unique_ptr우선 고려
마무리
핵심 요약
✅ unique_ptr: 기본 선택 (90%)
- 독점 소유권, 복사 불가, 이동만 가능
- 오버헤드 없음 ✅ shared_ptr: 공유 필요 시 (9%)
- 참조 카운팅, 여러 곳에서 소유
- 제어 블록 오버헤드 ✅ make_unique, make_shared: 항상 사용
- 예외 안전, 할당 최소화
실무 규칙
- 기본은 unique_ptr
- 정말 공유 필요할 때만 shared_ptr
- 원시 포인터는 non-owning 참조로만 (
get(),T*인자)
다음 글
- shared_ptr 고급 (enable_shared_from_this, aliasing, 스레드 안전성): C++ shared_ptr 고급 완벽 가이드
- weak_ptr과 순환 참조 해결: C++ 스마트 포인터와 순환 참조 해결법
정리
| 타입 | 소유권 | 복사 | 이동 | 오버헤드 | 사용 시점 |
|---|---|---|---|---|---|
| unique_ptr | 독점 | ❌ | ✅ | 없음 | 기본 선택 (90%) |
| shared_ptr | 공유 | ✅ | ✅ | 제어 블록 | 여러 곳에서 소유 |
| weak_ptr | 관찰 | ✅ | ✅ | 작음 | 순환 참조 방지 |
핵심 원칙:
- new/delete 대신 스마트 포인터 사용
- 기본은 unique_ptr, 공유 필요 시에만 shared_ptr
- make_unique/make_shared 사용 권장
- raw 포인터는 관찰만, 소유하지 않음
- 순환 참조는 weak_ptr로 해결
초보자를 위한 체크리스트
- 동적 할당 시 new 대신 make_unique 사용했는가?
- 여러 곳에서 소유해야 할 때만 shared_ptr을 썼는가?
- raw 포인터 반환은
get()으로만 하고 delete하지 않았는가?
💡 Tip: 문제 시나리오를 다시 읽고 싶다면 #problem-scenarios로 이동하세요.
자주 묻는 질문 (FAQ)
Q. unique_ptr과 shared_ptr 중 뭘 써야 할지 모르겠어요.
A. “여러 곳에서 소유해야 하나?”가 아니면 unique_ptr을 쓰세요. shared_ptr은 참조 카운팅 비용과 순환 참조 위험이 있으므로, 공유가 꼭 필요할 때만 사용합니다.
Q. 선행으로 읽으면 좋은 글은?
A. 메모리 누수, RAII를 먼저 읽으면 스마트 포인터의 배경을 이해하기 쉽습니다.
Q. 더 깊이 공부하려면?
A. cppreference - Smart pointers, “Effective Modern C++” (Scott Meyers) Item 18-22를 추천합니다.
참고 자료
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ shared_ptr 고급 완벽 가이드 | enable_shared_from_this·aliasing
- C++ 스마트 포인터 | 3일 동안 찾지 못한 순환 참조 버그 해결법
- C++ RAII | “파일을 열 수 없습니다” 장애의 원인과 자동 리소스 관리
- C++ 메모리 누수 | 서버 다운시킨 실제 사례와 Valgrind로 찾는 5가지 패턴
- C++ 스마트 포인터와 순환 참조(Circular Reference) 해결법 [#33-3]
이 글에서 다루는 키워드 (관련 검색어)
C++, 스마트포인터, unique_ptr, shared_ptr, make_unique, make_shared, 메모리관리, RAII, 메모리누수, 이중해제 등으로 검색하시면 이 글이 도움이 됩니다.
관련 글
- C++ unique_ptr 고급 완벽 가이드 | 커스텀 삭제자·배열
- C++ shared_ptr 고급 완벽 가이드 | enable_shared_from_this·aliasing
- C++ Google Test | gtest 설치부터 TEST·EXPECT_EQ
- C++ Google Mock |
- C++ shared_ptr vs unique_ptr |
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「C++ 스마트 포인터 기초 완벽 가이드 | unique_ptr·shared_ptr」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「C++ 스마트 포인터 기초 완벽 가이드 | unique_ptr·shared_ptr」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.