enable_shared_from_this: this를 shared_ptr로 넘기는 법과 bad_weak_ptr 함정

이 글의 핵심

this를 그냥 shared_ptr로 감싸면 왜 이중 해제가 나는지, enable_shared_from_this가 이를 어떻게 해결하는지, 생성자 호출 문제와 비동기 콜백에서의 실전 패턴을 정리합니다.

들어가며: “this를 shared_ptr로 어떻게 전달하나요?”

비동기 작업이나 콜백 함수에서 this 포인터를 전달해야 하는 경우가 많습니다. 하지만 raw this 포인터를 그대로 전달하면 객체가 이미 소멸된 후 접근하는 댕글링 포인터 문제가 발생합니다.

// ❌ 위험한 코드
class Widget {
public:
    void startAsync() {
        // 비동기 작업에 this 전달
        std::thread([this]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            this->process();  // ❌ Widget이 이미 소멸됐을 수 있음!
        }).detach();
    }
    
    void process() {
        std::cout << "Processing...\n";
    }
};

int main() {
    {
        Widget w;
        w.startAsync();
    }  // Widget 소멸
    
    std::this_thread::sleep_for(std::chrono::seconds(2));
    // 스레드가 소멸된 Widget에 접근 → 크래시!
}

이 글은 this를 shared_ptr로 바로 감싸면 왜 이중 삭제가 나는지에서 출발해, enable_shared_from_this가 내부의 weak_ptr로 이 문제를 푸는 방식, 생성자에서 호출하면 bad_weak_ptr이 나는 이유, C++17 weak_from_this(), 그리고 비동기 콜백과 Observer 패턴에서 쓰는 방법과 함정을 다룹니다.


실전 경험에서 배운 교훈

대규모 게임 엔진에서 비동기 리소스 로딩 시스템을 구현할 때, enable_shared_from_this는 필수였습니다. 초기에는 raw this 포인터를 콜백에 전달했다가, QA 단계에서 간헐적 크래시가 발생했습니다.

특히 문제가 되었던 것은:

  • 로딩 중 씬 전환: 리소스 로딩이 완료되기 전에 씬이 바뀌면서 객체가 소멸
  • 빠른 연속 클릭: 사용자가 빠르게 버튼을 클릭하면서 이전 작업이 완료되기 전에 새 작업 시작
  • 멀티스레드 환경: 여러 스레드에서 동시에 같은 객체에 접근

해결책:

  • enable_shared_from_this 상속으로 객체 수명 보장
  • weak_from_this()로 순환 참조 방지
  • 생성자 대신 팩토리 패턴으로 객체 생성

이렇게 바꾼 뒤로는 씬 전환 중 콜백이 이미 소멸된 객체에 접근하던 간헐적 크래시가 더 이상 재현되지 않았습니다.


문제: this를 shared_ptr로 변환할 수 없다

잘못된 접근: shared_ptr(this)

// ❌ 절대 하면 안 되는 코드
#include <memory>
#include <thread>
#include <iostream>

class Widget {
public:
    void startAsync() {
        // ❌ this를 shared_ptr로 변환 - 이중 삭제!
        auto self = std::shared_ptr<Widget>(this);
        
        std::thread([self]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            self->process();
        }).detach();
    }
    
    void process() {
        std::cout << "Processing...\n";
    }
};

int main() {
    auto widget = std::make_shared<Widget>();
    widget->startAsync();
    
    std::this_thread::sleep_for(std::chrono::seconds(2));
    // 크래시! 이중 삭제 (double delete)
}

문제점:

  • widget은 이미 shared_ptr로 관리되는 객체
  • shared_ptr<Widget>(this)는 새로운 제어 블록을 만듦
  • 같은 객체를 두 개의 독립적인 shared_ptr이 관리
  • 하나가 삭제하면, 다른 하나도 삭제 시도 → 이중 삭제 크래시

제어 블록이 중복됨

widget (shared_ptr)
  ↓
제어 블록 1 (ref_count = 1)
  ↓
Widget 객체 (메모리)
  ↑
제어 블록 2 (ref_count = 1)  ← shared_ptr<Widget>(this)
  ↑
self (shared_ptr)

// self의 ref_count가 0 → Widget 삭제
// widget의 ref_count가 0 → Widget 삭제 (이미 삭제됨!) → 크래시!

해결책: enable_shared_from_this

enable_shared_from_this 상속

enable_shared_from_this를 상속하면 기존 제어 블록을 재사용하는 shared_from_this()를 사용할 수 있습니다.

// ✅ enable_shared_from_this 상속
#include <memory>
#include <thread>
#include <iostream>

class Widget : public std::enable_shared_from_this<Widget> {
public:
    void startAsync() {
        // ✅ shared_from_this()로 안전하게 this를 shared_ptr로 변환
        auto self = shared_from_this();  // 기존 제어 블록 재사용
        
        std::thread([self]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            self->process();  // ✅ 안전! self가 객체 수명 보장
        }).detach();
    }
    
    void process() {
        std::cout << "Processing...\n";
    }
};

int main() {
    auto widget = std::make_shared<Widget>();
    widget->startAsync();
    
    std::this_thread::sleep_for(std::chrono::seconds(2));
    // ✅ 안전하게 동작!
}

내부 동작 원리

// enable_shared_from_this의 내부 구조 (개념적)
template <typename T>
class enable_shared_from_this {
private:
    mutable std::weak_ptr<T> weak_this_;  // weak_ptr로 제어 블록 참조
    
public:
    std::shared_ptr<T> shared_from_this() {
        // weak_ptr를 shared_ptr로 변환 (제어 블록 재사용)
        return std::shared_ptr<T>(weak_this_);
    }
    
    std::shared_ptr<const T> shared_from_this() const {
        return std::shared_ptr<const T>(weak_this_);
    }
};

// shared_ptr 생성자가 자동으로 weak_this_ 설정
// auto widget = std::make_shared<Widget>();
// → widget의 weak_this_가 제어 블록을 가리킴

제어 블록을 재사용

widget (shared_ptr, ref_count = 1)
  ↓
제어 블록 (ref_count = 1, weak_count = 1)
  ↓
Widget 객체 (메모리)
  ↑ weak_this_ (weak_ptr)

// shared_from_this() 호출 시:
self (shared_ptr, ref_count = 2)  ← 같은 제어 블록 사용!
  ↓
제어 블록 (ref_count = 2, weak_count = 1)

// 안전하게 동작!

shared_from_this() 사용법

shared_from_this()가 항상 새 제어 블록을 만드는 std::shared_ptr<T>(this)와 다른 이유는, enable_shared_from_this를 상속한 클래스가 내부에 weak_ptr<T> 멤버(weak_this_)를 하나 숨겨 갖고 있고, std::make_shared 또는 shared_ptr 생성자가 이 멤버를 자동으로 기존 제어 블록에 연결해 주기 때문입니다. 즉 shared_from_this()는 새로운 소유권을 만드는 게 아니라 “이미 존재하는 소유권을 하나 더 빌려오는” 연산이며, 이것이 use_count()가 1에서 2로 늘어나는 이유입니다 — 완전히 새로운 별개의 소유 그룹이 아니라 같은 그룹에 참조 하나가 추가된 것입니다.

기본 사용

#include <memory>
#include <iostream>

class Node : public std::enable_shared_from_this<Node> {
private:
    int value_;
    
public:
    explicit Node(int value) : value_(value) {
        std::cout << "Node(" << value_ << ") 생성\n";
    }
    
    ~Node() {
        std::cout << "Node(" << value_ << ") 소멸\n";
    }
    
    // ✅ this를 shared_ptr로 반환
    std::shared_ptr<Node> getShared() {
        return shared_from_this();
    }
    
    int getValue() const { return value_; }
};

int main() {
    auto node = std::make_shared<Node>(42);
    std::cout << "ref_count = " << node.use_count() << '\n';  // 1
    
    auto node2 = node->getShared();
    std::cout << "ref_count = " << node.use_count() << '\n';  // 2
    
    std::cout << "node2 value = " << node2->getValue() << '\n';
}

출력:

Node(42) 생성
ref_count = 1
ref_count = 2
node2 value = 42
Node(42) 소멸

비동기 작업에서 사용

이 예제의 출력에서 주목할 부분은 순서입니다: worker가 main의 중괄호 스코프를 벗어나 지역 변수로서는 소멸 대상이 되는데도 “Worker 1 소멸” 메시지가 그 즉시 출력되지 않고, “작업 완료” 메시지 다음에야 출력됩니다. 이것이 shared_from_this()가 실제로 하는 일입니다 — startWork() 안에서 캡처한 self(shared_ptr)가 스레드 람다 안에 살아있는 한, 원본 worker 변수가 스코프를 벗어나도 참조 카운트가 0이 되지 않으므로 객체는 계속 살아있습니다. self를 캡처하지 않고 this를 직접 캡처했다면 스레드가 실행되는 시점에 worker는 이미 소멸된 뒤일 수 있고, self->doWork()는 정의되지 않은 동작이 됩니다.

#include <memory>
#include <thread>
#include <iostream>
#include <chrono>

class AsyncWorker : public std::enable_shared_from_this<AsyncWorker> {
private:
    int id_;
    
public:
    explicit AsyncWorker(int id) : id_(id) {
        std::cout << "Worker " << id_ << " 생성\n";
    }
    
    ~AsyncWorker() {
        std::cout << "Worker " << id_ << " 소멸\n";
    }
    
    void startWork() {
        // ✅ shared_from_this()로 객체 수명 보장
        auto self = shared_from_this();
        
        std::thread([self]() {
            std::this_thread::sleep_for(std::chrono::seconds(2));
            self->doWork();
        }).detach();
        
        std::cout << "Worker " << id_ << " 작업 시작\n";
    }
    
private:
    void doWork() {
        std::cout << "Worker " << id_ << " 작업 완료\n";
    }
};

int main() {
    {
        auto worker = std::make_shared<AsyncWorker>(1);
        worker->startWork();
    }  // worker 소멸 - 하지만 스레드가 shared_ptr 보유 중
    
    std::cout << "main: worker 스코프 벗어남\n";
    
    std::this_thread::sleep_for(std::chrono::seconds(3));
    std::cout << "main: 종료\n";
}

출력:

Worker 1 생성
Worker 1 작업 시작
main: worker 스코프 벗어남
Worker 1 작업 완료
Worker 1 소멸
main: 종료

생성자에서 호출 시 문제

bad_weak_ptr 예외

생성자에서 shared_from_this()를 호출하면 std::bad_weak_ptr 예외가 발생합니다.

// ❌ 생성자에서 shared_from_this() 호출
#include <memory>
#include <iostream>

class Widget : public std::enable_shared_from_this<Widget> {
public:
    Widget() {
        try {
            auto self = shared_from_this();  // ❌ 예외 발생!
        } catch (const std::bad_weak_ptr& e) {
            std::cout << "예외: " << e.what() << '\n';
        }
    }
};

int main() {
    auto widget = std::make_shared<Widget>();
}

출력:

예외: bad_weak_ptr

왜 발생하는가?

// shared_ptr 생성 순서
auto widget = std::make_shared<Widget>();

// 1. 메모리 할당
// 2. Widget 생성자 호출  ← 이 시점에는 아직 shared_ptr 미완성!
// 3. 제어 블록 초기화
// 4. weak_this_ 설정      ← 생성자 이후에 설정됨
// 5. shared_ptr 완성

// 생성자에서 shared_from_this() 호출 시:
// weak_this_가 아직 설정되지 않음 → bad_weak_ptr 예외!

해결책 1: 팩토리 패턴

// ✅ 팩토리 패턴으로 해결
#include <memory>
#include <iostream>

class Widget : public std::enable_shared_from_this<Widget> {
private:
    // private 생성자
    Widget() {
        std::cout << "Widget 생성\n";
    }
    
public:
    // ✅ 팩토리 메서드
    static std::shared_ptr<Widget> create() {
        // shared_ptr 생성
        auto widget = std::shared_ptr<Widget>(new Widget());
        
        // 초기화 (생성자가 아니므로 안전)
        widget->initialize();
        
        return widget;
    }
    
private:
    void initialize() {
        // ✅ 생성자가 아니므로 shared_from_this() 사용 가능
        auto self = shared_from_this();
        std::cout << "초기화 완료, ref_count = " << self.use_count() << '\n';
    }
};

int main() {
    auto widget = Widget::create();
}

출력:

Widget 생성
초기화 완료, ref_count = 2

해결책 2: init() 메서드 분리

// ✅ 초기화 메서드 분리
class Widget : public std::enable_shared_from_this<Widget> {
public:
    Widget() {
        std::cout << "Widget 생성\n";
    }
    
    // ✅ 생성 후 반드시 호출
    void init() {
        auto self = shared_from_this();
        // 초기화 작업
        startAsyncTasks();
    }
    
private:
    void startAsyncTasks() {
        std::cout << "비동기 작업 시작\n";
    }
};

int main() {
    auto widget = std::make_shared<Widget>();
    widget->init();  // 반드시 호출
}

weak_from_this() (C++17)

weak_from_this() 소개

C++17부터 weak_from_this()를 사용할 수 있습니다. 순환 참조 방지나 객체 존재 확인에 유용합니다.

// ✅ weak_from_this() (C++17)
#include <memory>
#include <iostream>

class Node : public std::enable_shared_from_this<Node> {
private:
    int value_;
    
public:
    explicit Node(int value) : value_(value) {}
    
    // ✅ weak_ptr 반환
    std::weak_ptr<Node> getWeak() {
        return weak_from_this();  // C++17
    }
    
    void process() {
        std::cout << "Processing " << value_ << '\n';
    }
};

int main() {
    std::weak_ptr<Node> weakNode;
    
    {
        auto node = std::make_shared<Node>(42);
        weakNode = node->getWeak();
        
        // weak_ptr → shared_ptr 변환 (lock)
        if (auto shared = weakNode.lock()) {
            shared->process();  // ✅ 안전
        }
    }  // node 소멸
    
    // 객체가 소멸된 후
    if (auto shared = weakNode.lock()) {
        shared->process();
    } else {
        std::cout << "Node가 이미 소멸됨\n";  // ✅ 이 경로
    }
}

출력:

Processing 42
Node가 이미 소멸됨

Observer 패턴에서 활용

// ✅ Observer 패턴 - 순환 참조 방지
#include <memory>
#include <vector>
#include <iostream>

class Observer : public std::enable_shared_from_this<Observer> {
private:
    int id_;
    
public:
    explicit Observer(int id) : id_(id) {
        std::cout << "Observer " << id_ << " 생성\n";
    }
    
    ~Observer() {
        std::cout << "Observer " << id_ << " 소멸\n";
    }
    
    void notify(const std::string& event) {
        std::cout << "Observer " << id_ << " 이벤트: " << event << '\n';
    }
    
    // ✅ weak_ptr 반환 (순환 참조 방지)
    std::weak_ptr<Observer> getWeak() {
        return weak_from_this();
    }
};

class Subject {
private:
    std::vector<std::weak_ptr<Observer>> observers_;  // ✅ weak_ptr로 저장
    
public:
    void addObserver(std::weak_ptr<Observer> observer) {
        observers_.push_back(observer);
    }
    
    void notifyAll(const std::string& event) {
        // 소멸된 Observer 제거
        observers_.erase(
            std::remove_if(observers_.begin(), observers_.end(),
                [](const std::weak_ptr<Observer>& weak) {
                    return weak.expired();
                }),
            observers_.end()
        );
        
        // 살아있는 Observer에게 알림
        for (auto& weakObs : observers_) {
            if (auto obs = weakObs.lock()) {
                obs->notify(event);
            }
        }
    }
};

int main() {
    Subject subject;
    
    {
        auto obs1 = std::make_shared<Observer>(1);
        auto obs2 = std::make_shared<Observer>(2);
        
        subject.addObserver(obs1->getWeak());
        subject.addObserver(obs2->getWeak());
        
        subject.notifyAll("이벤트 1");
    }  // obs1, obs2 소멸
    
    std::cout << "\n소멸 후 알림:\n";
    subject.notifyAll("이벤트 2");  // 아무도 받지 않음
}

출력:

Observer 1 생성
Observer 2 생성
Observer 1 이벤트: 이벤트 1
Observer 2 이벤트: 이벤트 1
Observer 1 소멸
Observer 2 소멸

소멸 후 알림:

실전 패턴: 비동기 작업

앞서 다룬 원리를 실제로 자주 마주치는 두 가지 상황 — 콜백을 받는 비동기 요청, 반복 실행되는 타이머 — 에 적용해 보면 왜 enable_shared_from_this가 라이브러리 설계에서 거의 필수적인지가 드러납니다. 두 경우 모두 공통점은 “객체의 생성 시점과 콜백이 실제로 실행되는 시점 사이에 시간차가 있고, 그 사이 원본 객체의 수명이 호출자의 제어를 벗어난다”는 것입니다.

HTTP 클라이언트

getAsync를 호출한 코드가 client 변수를 스코프 밖으로 내보내도(main의 중괄호가 끝나도) 콜백이 정상적으로 실행되는 이유는 self가 스레드 람다에 캡처되어 HttpClient 객체의 소유권을 하나 더 쥐고 있기 때문입니다. 만약 self 대신 this를 직접 캡처했다면, “요청 전송됨”이 출력된 후 client가 소멸되고, 1초 뒤 스레드가 깨어나 self->url_(사실상 댕글링 포인터를 통한 접근)에 접근하는 순간 정의되지 않은 동작이 발생합니다 — 대부분의 경우 크래시로 나타나지만, 운이 나쁘면 조용히 잘못된 값을 읽어 디버깅을 훨씬 어렵게 만듭니다.

// ✅ 비동기 HTTP 클라이언트
#include <memory>
#include <thread>
#include <iostream>
#include <functional>
#include <chrono>

class HttpClient : public std::enable_shared_from_this<HttpClient> {
private:
    std::string url_;
    
public:
    explicit HttpClient(const std::string& url) : url_(url) {}
    
    using ResponseCallback = std::function<void(const std::string&)>;
    
    // ✅ 비동기 요청
    void getAsync(ResponseCallback callback) {
        // shared_from_this()로 객체 수명 보장
        auto self = shared_from_this();
        
        std::thread([self, callback]() {
            // 네트워크 요청 시뮬레이션
            std::this_thread::sleep_for(std::chrono::seconds(1));
            
            std::string response = "Response from " + self->url_;
            callback(response);
        }).detach();
    }
};

int main() {
    {
        auto client = std::make_shared<HttpClient>("https://api.example.com");
        
        client->getAsync([](const std::string& response) {
            std::cout << "콜백: " << response << '\n';
        });
        
        std::cout << "요청 전송됨\n";
    }  // client 스코프 벗어남 - 하지만 스레드가 보유 중
    
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "main 종료\n";
}

출력:

요청 전송됨
콜백: Response from https://api.example.com
main 종료

타이머 이벤트 핸들러

이 예제는 shared_from_this()만으로는 완전한 해결책이 아니라는 점을 보여줍니다. self가 Timer 객체의 메모리 자체는 살려주지만, “타이머를 계속 반복시킬 것인가”는 별개의 논리적 질문입니다. stopped_ 플래그와 소멸자에서의 stopped_ = true 설정이 바로 그 역할을 합니다 — 객체가 소멸을 시작해도(스코프를 벗어나 ~Timer()가 호출되어도) 메모리 자체는 스레드가 든 self 덕분에 아직 해제되지 않지만, stopped_를 확인해 반복문을 즉시 종료시킴으로써 “논리적으로 죽은” 타이머가 불필요하게 계속 틱을 발생시키는 것을 막습니다. 즉 메모리 수명 관리(shared_ptr)와 애플리케이션 로직상의 생명주기(stopped_ 플래그)는 별개의 문제이며, 둘 다 신경 써야 실제로 올바른 비동기 코드가 됩니다.

// ✅ 타이머 이벤트 핸들러
#include <memory>
#include <thread>
#include <iostream>
#include <chrono>
#include <atomic>

class Timer : public std::enable_shared_from_this<Timer> {
private:
    int id_;
    std::atomic<bool> stopped_{false};
    
public:
    explicit Timer(int id) : id_(id) {}
    
    ~Timer() {
        stopped_ = true;
    }
    
    void start(int intervalMs, int repeatCount) {
        auto self = shared_from_this();
        
        std::thread([self, intervalMs, repeatCount]() {
            for (int i = 0; i < repeatCount && !self->stopped_; ++i) {
                std::this_thread::sleep_for(std::chrono::milliseconds(intervalMs));
                self->onTimer(i + 1);
            }
        }).detach();
    }
    
private:
    void onTimer(int tick) {
        std::cout << "Timer " << id_ << " tick " << tick << '\n';
    }
};

int main() {
    {
        auto timer = std::make_shared<Timer>(1);
        timer->start(500, 5);  // 500ms마다 5번
        
        std::this_thread::sleep_for(std::chrono::seconds(2));
    }  // timer 소멸 - 하지만 stopped_ = true로 안전하게 종료
    
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "main 종료\n";
}

주의사항과 함정

스택 객체에서 사용 불가

이 실패는 앞서 4장에서 설명한 것과 근본적으로 같은 원인입니다 — weak_this_는 오직 shared_ptr/make_shared를 통해 객체가 만들어질 때만 설정되는데, 스택에 선언된 Widget widget은 애초에 그런 경로를 거치지 않으므로 weak_this_가 영원히 비어 있는 상태입니다. 생성자 문제(4장)는 “아직 설정 전”이라 예외가 나는 것이고, 이 경우는 “설정될 방법 자체가 없다”는 점이 다르지만, 증상(bad_weak_ptr)은 동일합니다.

// ❌ 스택 객체에서 shared_from_this() 호출
#include <memory>

class Widget : public std::enable_shared_from_this<Widget> {
public:
    void test() {
        auto self = shared_from_this();  // ❌ bad_weak_ptr!
    }
};

int main() {
    Widget widget;  // 스택 객체 (shared_ptr 아님)
    // widget.test();  // bad_weak_ptr 예외!
}

해결책: 항상 std::make_shared로 생성.

다중 상속 시 모호성

Derived가 Base1과 Base2를 둘 다 상속하면, 각 베이스가 자신만의 enable_shared_from_this<Base1>/<Base2> 서브객체와 그 안의 weak_this_를 독립적으로 갖게 됩니다. Derived 객체에서 shared_from_this()를 호출하려 하면 컴파일러는 “Base1의 것인지 Base2의 것인지” 판단할 근거가 없어 모호성 컴파일 에러를 냅니다. 이는 컴파일 타임에 잡히므로 런타임 버그로 이어지진 않지만, 애초에 enable_shared_from_this를 상속 계층 중 정확히 한 곳(보통 가장 파생된 클래스, 또는 공통 베이스 하나)에서만 상속하도록 설계하는 것이 유일하게 모호하지 않은 해법입니다.

// ❌ 다중 enable_shared_from_this 상속
class Base1 : public std::enable_shared_from_this<Base1> {};
class Base2 : public std::enable_shared_from_this<Base2> {};

class Derived : public Base1, public Base2 {  // ❌ 모호성!
    // shared_from_this()가 어느 것인지 모호함
};

해결책: 한 번만 상속.

// ✅ 한 번만 상속
class Base {};
class Derived : public Base, 
                public std::enable_shared_from_this<Derived> {
    // ✅ 명확함
};

소멸자에서 shared_from_this() 호출

소멸자가 실행되고 있다는 것은 이미 참조 카운트가 0에 도달해 제어 블록이 “삭제 진행 중” 상태로 전환됐다는 뜻입니다. 이 시점에 shared_from_this()로 새 shared_ptr을 만들면, 이미 0이 된(혹은 소멸 절차에 들어간) 카운트를 기반으로 또 다른 소유권 참조를 만들어내려는 시도가 되어 정의되지 않은 동작의 위험이 있습니다. 실무 규칙은 단순합니다 — 소멸자 안에서는 shared_from_this()를 아예 호출하지 않고, 정리 작업이 필요하다면 소멸자가 아니라 명시적인 close()/shutdown() 메서드에서 처리해 호출 시점을 객체가 확실히 살아있는 동안으로 옮기는 것입니다.

// ⚠️ 소멸자에서 호출 가능하지만 조심
class Widget : public std::enable_shared_from_this<Widget> {
public:
    ~Widget() {
        // ⚠️ 소멸 중이므로 위험
        // auto self = shared_from_this();  // 가능하지만 권장 안 함
    }
};

순환 참조 주의

n->next = shared_from_this()가 만드는 순환은 전형적인 shared_ptr 순환 참조입니다 — 노드 A가 노드 B를 shared_ptr로 가리키고, B도 A를 shared_ptr로 가리키면 두 객체의 참조 카운트는 서로를 붙잡고 있어 절대 0이 되지 않고, 외부에서 두 노드에 대한 참조를 모두 잃어도 메모리는 영원히 해제되지 않는 누수가 됩니다(enable_shared_from_this가 없어도 shared_ptr 두 개가 서로를 가리키기만 하면 발생하는 문제이지만, shared_from_this()로 자기 자신에 대한 참조를 순환 구조에 끼워 넣기 쉬워 이 클래스를 쓸 때 특히 자주 마주칩니다). 해법은 순환의 한쪽 방향을 weak_ptr로 바꾸는 것 — 소유권은 한 방향으로만 흐르게 하고, 반대 방향은 “소유하지 않고 참조만 하는” weak_ptr로 표현해 참조 카운트에 기여하지 않도록 끊어주는 것입니다. 이후 8장의 부모-자식 예제도 같은 원리를 트리 구조 전체에 일관되게 적용한 것입니다.

// ❌ 순환 참조 발생
class Node : public std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> next;  // ❌ shared_ptr
    
    void setNext(std::shared_ptr<Node> n) {
        next = n;
        n->next = shared_from_this();  // ❌ 순환 참조!
    }
};

// ✅ weak_ptr로 해결
class Node : public std::enable_shared_from_this<Node> {
public:
    std::weak_ptr<Node> next;  // ✅ weak_ptr
    
    void setNext(std::shared_ptr<Node> n) {
        next = n;
        if (auto shared = n->next.lock()) {
            // ...
        }
    }
};

실전 베스트 프랙티스

아래 네 가지는 개별 기법이라기보다, 지금까지 다룬 실패 사례들(생성자 호출 예외, 순환 참조, 암묵적 캡처 실수, 부적절한 수명 관리)을 클래스 설계 단계에서 원천 차단하는 습관입니다.

팩토리 패턴 사용

생성자를 private으로 막고 정적 create() 메서드만 노출하면, “shared_ptr이 아닌 방식으로 이 클래스를 생성하는 것”과 “생성자 안에서 shared_from_this()를 호출하는 것” 두 가지 실수 경로를 모두 컴파일 타임에 막을 수 있습니다. initialize()가 생성자가 아니라 create() 내부에서, new Widget()으로 객체를 만들고 shared_ptr으로 감싼 이후에 호출되므로 이 시점에는 weak_this_가 이미 설정되어 있어 안전합니다.

// ✅ 팩토리 패턴으로 생성 강제
class AsyncTask : public std::enable_shared_from_this<AsyncTask> {
private:
    AsyncTask() = default;  // private 생성자
    
public:
    static std::shared_ptr<AsyncTask> create() {
        auto task = std::shared_ptr<AsyncTask>(new AsyncTask());
        task->initialize();
        return task;
    }
    
private:
    void initialize() {
        auto self = shared_from_this();
        // 초기화 작업
    }
};

weak_ptr로 순환 참조 방지

// ✅ weak_ptr 활용
class Parent : public std::enable_shared_from_this<Parent> {
public:
    std::vector<std::weak_ptr<Child>> children;  // weak_ptr
};

class Child : public std::enable_shared_from_this<Child> {
public:
    std::weak_ptr<Parent> parent;  // weak_ptr
};

람다 캡처 시 명시적으로

[this]로 캡처하면 컴파일은 되지만 원본 객체의 수명을 전혀 보장하지 못하는 raw 포인터를 캡처하는 것과 다르지 않습니다 — 이 글 전체에서 반복해서 강조한 문제로 되돌아가는 셈입니다. auto self = shared_from_this();를 먼저 지역 변수로 만든 뒤 [self]로 캡처하면, 코드 리뷰어가 “이 람다가 객체 수명을 어떻게 보장하는지”를 self 한 단어만 보고도 즉시 알 수 있다는 가독성 이점도 있습니다.

// ✅ 명시적으로 shared_ptr 캡처
class Worker : public std::enable_shared_from_this<Worker> {
public:
    void startWork() {
        auto self = shared_from_this();  // 명시적으로 shared_ptr 생성
        
        std::thread([self]() {  // 람다로 캡처
            self->doWork();
        }).detach();
    }
};

RAII로 수명 관리

// ✅ RAII 패턴
class Resource : public std::enable_shared_from_this<Resource> {
public:
    class Handle {
    private:
        std::shared_ptr<Resource> resource_;
        
    public:
        explicit Handle(std::shared_ptr<Resource> res)
            : resource_(std::move(res)) {
            resource_->acquire();
        }
        
        ~Handle() {
            if (resource_) {
                resource_->release();
            }
        }
    };
    
    Handle getHandle() {
        return Handle(shared_from_this());
    }
    
private:
    void acquire() { /* 리소스 획득 */ }
    void release() { /* 리소스 해제 */ }
};

정리 및 결론

핵심 정리

항목설명
용도this를 shared_ptr로 안전하게 변환
상속std::enable_shared_from_this<T>
메서드shared_from_this(), weak_from_this() (C++17)
주의생성자에서 호출 불가 (bad_weak_ptr)
필수shared_ptr로 관리되는 객체에서만 사용

사용 시나리오

// 1. 비동기 작업
class AsyncWorker : public std::enable_shared_from_this<AsyncWorker> {
public:
    void startAsync() {
        auto self = shared_from_this();
        std::thread([self]() { self->work(); }).detach();
    }
};

// 2. 콜백 등록
class EventHandler : public std::enable_shared_from_this<EventHandler> {
public:
    void registerCallback() {
        auto self = shared_from_this();
        eventSystem.on("click", [self](Event e) {
            self->handleClick(e);
        });
    }
};

// 3. Observer 패턴
class Observer : public std::enable_shared_from_this<Observer> {
public:
    std::weak_ptr<Observer> getWeak() {
        return weak_from_this();  // 순환 참조 방지
    }
};

체크리스트

enable_shared_from_this를 사용하기 전에:

  • 객체가 shared_ptr로 관리되는가?
  • 생성자에서 shared_from_this() 호출하지 않는가?
  • 스택 객체가 아닌가?
  • 다중 상속으로 모호성이 없는가?
  • 순환 참조를 weak_ptr로 방지하는가?

같이 보면 좋은 글