[2026] C++ Observer Pointer | 관찰 포인터 가이드
이 글의 핵심
C++ Observer Pointer: 관찰 포인터 가이드. 관찰 포인터 기본·사용 패턴.
들어가며
관찰 포인터(Observer Pointer)는 소유권 없이 객체를 참조만 하는 포인터입니다. 스마트 포인터가 소유권을 관리하는 반면, 관찰 포인터는 객체의 수명에 관여하지 않고 단순히 관찰만 합니다.
실무에서 마주한 현실
개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.
1. 관찰 포인터 기본
소유권 vs 관찰
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 부모-자식 관계
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 콜백
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 컨테이너 요소 접근
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 댕글링 포인터
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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 체크 누락
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <iostream>
void process(Widget* ptr) {
// ❌ nullptr 체크 없음
// ptr->use(); // ptr이 nullptr이면 크래시!
// ✅ 항상 nullptr 체크
if (!ptr) {
std::cout << "널 포인터" << std::endl;
return;
}
ptr->use();
}
문제 3: 소유권 혼동
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 컨테이너 저장
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <vector>
#include <memory>
// ❌ raw pointer 컨테이너 (소유권 불명확)
std::vector<Widget*> widgets;
// 누가 delete? 메모리 누수 가능
// ✅ 소유 포인터 컨테이너
std::vector<std::unique_ptr<Widget>> owners;
// ✅ 관찰 포인터 컨테이너 (소유권은 다른 곳)
std::vector<Widget*> observers;
4. 실전 예제: 이벤트 시스템
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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;
}
정리
핵심 요약
- 관찰 포인터: 소유권 없는 포인터
- 용도: 부모 참조, 콜백, 임시 접근
- 위험: 댕글링 포인터 (수명 관리 주의)
- nullptr 체크: 필수
- 소유권 명확화:
unique_ptr/shared_ptrvs raw pointer
포인터 타입 비교
| 타입 | 소유권 | 수명 관리 | 사용 시기 |
|---|---|---|---|
unique_ptr | 단독 소유 | 자동 | 명확한 소유권 |
shared_ptr | 공유 소유 | 참조 카운트 | 여러 소유자 |
| raw pointer | 없음 (관찰) | 수동 | 참조만 |
weak_ptr | 없음 | shared_ptr 관찰 | 순환 참조 방지 |
실전 팁
사용 원칙:
- 소유권 있으면 스마트 포인터
- 소유권 없으면 raw pointer (관찰)
- 소유권 불명확하면 설계 재검토 안전성:
- 항상
nullptr체크 - 소유 포인터 수명 내에서만 사용
- 댕글링 포인터 주의 가독성:
- 함수 시그니처에 소유권 명시
- 주석으로 소유권 문서화
observer_ptr<T>타입 별칭 고려