C++ Observer Pointer | "관찰 포인터" 가이드

C++ Observer Pointer | "관찰 포인터" 가이드

이 글의 핵심

C++ Observer Pointer에 대한 실전 가이드입니다.

들어가며

관찰 포인터(Observer Pointer)는 소유권 없이 객체를 참조만 하는 포인터입니다. 스마트 포인터가 소유권을 관리하는 반면, 관찰 포인터는 객체의 수명에 관여하지 않고 단순히 관찰만 합니다.


1. 관찰 포인터 기본

소유권 vs 관찰

#include <memory>
#include <iostream>

class Widget {
public:
    Widget() {
        std::cout << "Widget 생성" << std::endl;
    }
    
    ~Widget() {
        std::cout << "Widget 소멸" << std::endl;
    }
    
    void use() {
        std::cout << "Widget 사용" << std::endl;
    }
};

int main() {
    // 소유권 있음
    std::unique_ptr<Widget> owner = std::make_unique<Widget>();
    
    // 소유권 없음 (관찰만)
    Widget* observer = owner.get();
    
    observer->use();  // OK
    
    // owner가 소멸되면 observer는 댕글링 포인터
    
    return 0;
}

핵심 개념:

  • 소유 포인터: 객체 수명 관리 (unique_ptr, shared_ptr)
  • 관찰 포인터: 객체 참조만, 수명 관리 안 함 (raw pointer)

2. 사용 패턴

패턴 1: 부모-자식 관계

#include <memory>
#include <vector>
#include <iostream>

class Parent;

class Child {
    Parent* parent;  // 관찰 포인터 (부모 참조)
    
public:
    Child() : parent(nullptr) {}
    
    void setParent(Parent* p) {
        parent = p;
    }
    
    void notifyParent() {
        if (parent) {
            std::cout << "부모에게 알림" << std::endl;
        }
    }
};

class Parent {
    std::vector<std::unique_ptr<Child>> children;  // 소유 포인터
    
public:
    void addChild(std::unique_ptr<Child> child) {
        child->setParent(this);  // this는 관찰 포인터
        children.push_back(std::move(child));
    }
    
    size_t childCount() const {
        return children.size();
    }
};

int main() {
    Parent parent;
    
    auto child1 = std::make_unique<Child>();
    auto child2 = std::make_unique<Child>();
    
    parent.addChild(std::move(child1));
    parent.addChild(std::move(child2));
    
    std::cout << "자식 수: " << parent.childCount() << std::endl;
    
    return 0;
}

패턴 2: 콜백

#include <iostream>
#include <functional>

class Button {
public:
    using ClickHandler = std::function<void(Button*)>;
    
    void setOnClick(ClickHandler handler) {
        onClick = handler;
    }
    
    void click() {
        if (onClick) {
            onClick(this);  // this는 관찰 포인터
        }
    }
    
private:
    ClickHandler onClick;
};

int main() {
    Button btn;
    
    btn.setOnClick( {
        std::cout << "버튼 클릭됨" << std::endl;
    });
    
    btn.click();
    
    return 0;
}

패턴 3: 컨테이너 요소 접근

#include <memory>
#include <vector>
#include <iostream>

class Item {
    int id;
    
public:
    Item(int i) : id(i) {}
    
    int getId() const { return id; }
};

class Container {
    std::vector<std::unique_ptr<Item>> items;
    
public:
    void add(std::unique_ptr<Item> item) {
        items.push_back(std::move(item));
    }
    
    // 소유권 유지, 관찰 포인터 반환
    Item* find(int id) {
        for (auto& item : items) {
            if (item->getId() == id) {
                return item.get();  // 관찰 포인터
            }
        }
        return nullptr;
    }
    
    // const 버전
    const Item* find(int id) const {
        for (const auto& item : items) {
            if (item->getId() == id) {
                return item.get();
            }
        }
        return nullptr;
    }
};

int main() {
    Container container;
    container.add(std::make_unique<Item>(1));
    container.add(std::make_unique<Item>(2));
    container.add(std::make_unique<Item>(3));
    
    Item* item = container.find(2);
    if (item) {
        std::cout << "찾음: " << item->getId() << std::endl;
    }
    
    return 0;
}

3. 자주 발생하는 문제

문제 1: 댕글링 포인터

#include <memory>
#include <iostream>

Widget* observer;

void bad() {
    auto owner = std::make_unique<Widget>();
    observer = owner.get();
}  // owner 소멸 -> observer는 댕글링!

void good() {
    auto owner = std::make_unique<Widget>();
    Widget* localObserver = owner.get();
    localObserver->use();  // owner 수명 내에서 사용
}

int main() {
    bad();
    // observer->use();  // 정의되지 않은 동작!
    
    good();  // 안전
    
    return 0;
}

해결책: 관찰 포인터는 소유 포인터의 수명 내에서만 사용하세요.

문제 2: nullptr 체크 누락

#include <iostream>

void process(Widget* ptr) {
    // ❌ nullptr 체크 없음
    // ptr->use();  // ptr이 nullptr이면 크래시!
    
    // ✅ 항상 nullptr 체크
    if (!ptr) {
        std::cout << "널 포인터" << std::endl;
        return;
    }
    
    ptr->use();
}

문제 3: 소유권 혼동

#include <memory>

// ❌ raw pointer로 소유권 이전 (불명확)
Widget* createWidget() {
    return new Widget();  // 누가 delete?
}

// ✅ unique_ptr로 소유권 명확
std::unique_ptr<Widget> createWidget() {
    return std::make_unique<Widget>();
}

// ✅ 관찰 포인터 반환 (소유권 유지)
class Manager {
    std::vector<std::unique_ptr<Widget>> widgets;
    
public:
    Widget* getWidget(size_t index) {
        return widgets[index].get();  // 관찰만
    }
};

문제 4: 컨테이너 저장

#include <vector>
#include <memory>

// ❌ raw pointer 컨테이너 (소유권 불명확)
std::vector<Widget*> widgets;
// 누가 delete? 메모리 누수 가능

// ✅ 소유 포인터 컨테이너
std::vector<std::unique_ptr<Widget>> owners;

// ✅ 관찰 포인터 컨테이너 (소유권은 다른 곳)
std::vector<Widget*> observers;

4. 실전 예제: 이벤트 시스템

#include <memory>
#include <vector>
#include <iostream>
#include <algorithm>

class Event {
public:
    std::string type;
    
    Event(const std::string& t) : type(t) {}
};

class EventListener {
public:
    virtual ~EventListener() = default;
    virtual void onEvent(const Event& event) = 0;
};

class EventDispatcher {
    std::vector<EventListener*> listeners;  // 관찰 포인터
    
public:
    // 리스너 등록 (소유권 없음)
    void addListener(EventListener* listener) {
        if (listener) {
            listeners.push_back(listener);
        }
    }
    
    // 리스너 제거
    void removeListener(EventListener* listener) {
        listeners.erase(
            std::remove(listeners.begin(), listeners.end(), listener),
            listeners.end()
        );
    }
    
    // 이벤트 발송
    void dispatch(const Event& event) {
        for (EventListener* listener : listeners) {
            if (listener) {
                listener->onEvent(event);
            }
        }
    }
};

class Logger : public EventListener {
public:
    void onEvent(const Event& event) override {
        std::cout << "[LOG] 이벤트: " << event.type << std::endl;
    }
};

class Counter : public EventListener {
    int count = 0;
    
public:
    void onEvent(const Event& event) override {
        count++;
        std::cout << "[COUNT] 총 " << count << "개 이벤트" << std::endl;
    }
};

int main() {
    EventDispatcher dispatcher;
    
    // 리스너 생성 (소유권 유지)
    Logger logger;
    Counter counter;
    
    // 관찰 포인터로 등록
    dispatcher.addListener(&logger);
    dispatcher.addListener(&counter);
    
    // 이벤트 발송
    dispatcher.dispatch(Event{"UserLogin"});
    dispatcher.dispatch(Event{"DataSaved"});
    
    // 리스너 제거
    dispatcher.removeListener(&logger);
    
    dispatcher.dispatch(Event{"UserLogout"});
    
    return 0;
}

정리

핵심 요약

  1. 관찰 포인터: 소유권 없는 포인터
  2. 용도: 부모 참조, 콜백, 임시 접근
  3. 위험: 댕글링 포인터 (수명 관리 주의)
  4. nullptr 체크: 필수
  5. 소유권 명확화: unique_ptr/shared_ptr vs raw pointer

포인터 타입 비교

타입소유권수명 관리사용 시기
unique_ptr단독 소유자동명확한 소유권
shared_ptr공유 소유참조 카운트여러 소유자
raw pointer없음 (관찰)수동참조만
weak_ptr없음shared_ptr 관찰순환 참조 방지

실전 팁

사용 원칙:

  • 소유권 있으면 스마트 포인터
  • 소유권 없으면 raw pointer (관찰)
  • 소유권 불명확하면 설계 재검토

안전성:

  • 항상 nullptr 체크
  • 소유 포인터 수명 내에서만 사용
  • 댕글링 포인터 주의

가독성:

  • 함수 시그니처에 소유권 명시
  • 주석으로 소유권 문서화
  • observer_ptr<T> 타입 별칭 고려

다음 단계

  • C++ Smart Pointers
  • C++ nullptr vs NULL
  • C++ weak_ptr

관련 글

  • C++ nullptr vs NULL |
  • C++ nullptr |
  • C++ 포인터 |
  • C++ this Pointer |
  • 배열과 리스트 | 코딩 테스트 필수 자료구조 완벽 정리