C++ 디자인 패턴 종합 가이드 | Singleton·Factory
이 글의 핵심
전역 설정 접근, 객체 생성 분기, 이벤트 전파, 알고리즘 교체, 빌드 시간 폭증—실제 겪는 문제를 Singleton·Factory·Observer·Strategy·PIMPL로 해결하는 방법. 완전한 예제, 자주 하는 실수, 프로덕션 패턴까지.
들어가며: 패턴 없이 코딩하면 어떤 문제가 생기나요?
”설정은 전역 변수, 생성은 if-else, 알림은 직접 호출—나중에 수정이 너무 어려워요”
디자인 패턴은 “반복되는 설계 문제에 대한 검증된 해법”입니다. 패턴을 모르고 코딩해도 동작은 합니다. 하지만 확장·테스트·유지보수가 점점 어려워집니다. “한 줄만 바꿨는데 50개 파일이 재컴파일된다”, “새 알림 채널을 추가하려니 10곳을 수정해야 한다” 같은 경험이 있다면, 이 글의 패턴들이 해결책이 됩니다.
비유하면: 패턴은 “레시피”와 같습니다. 요리(코딩)는 레시피 없이도 할 수 있지만, 레시피를 따르면 실패 확률이 줄고, 다른 사람과 협업할 때도 “이건 팩토리 패턴으로 했어요”라고 말하면 의도가 바로 전달됩니다.
이 글을 읽으면:
- Singleton, Factory, Observer, Strategy, PIMPL의 완전한 예제를 볼 수 있습니다.
- 각 패턴이 해결하는 구체적인 문제 시나리오를 이해할 수 있습니다.
- 자주 하는 실수와 해결법을 익힐 수 있습니다.
- 프로덕션에서 바로 적용할 수 있는 패턴을 배울 수 있습니다.
같은 패턴을 JavaScript·Python (데코레이터·싱글톤 예시)에서도 접할 수 있습니다. 시리즈로는 생성 패턴·구조 패턴·행동 패턴과 연결해 읽을 수 있습니다. ABI·레거시 관점의 실무 적용은 PIMPL·인터페이스·레거시 현대화 글을 참고하세요.
목차
- 문제 시나리오
- Singleton 패턴
- Factory 패턴
- Observer 패턴
- Strategy 패턴
- PIMPL 패턴
- 자주 발생하는 에러와 해결법
- 모범 사례
- 프로덕션 패턴
- 패턴 선택 가이드와 체크리스트
1. 문제 시나리오
시나리오 1: “설정 객체를 어디서든 접근하고 싶어요”
"로그 설정, DB 연결 정보, API 키가 앱 전체에서 하나만 있어야 해요."
"전역 변수로 두면 테스트 시 mock으로 바꾸기 어렵고, 초기화 순서가 꼬여요."
상황: 여러 모듈에서 같은 설정 객체에 접근해야 합니다. 전역 변수는 “언제 초기화되는지” 불명확하고, 정적 초기화 순서 fiasco로 다른 전역 객체가 아직 초기화되지 않은 상태에서 접근할 수 있습니다.
해결 포인트: Singleton으로 단일 인스턴스를 보장하고, lazy initialization으로 최초 사용 시점에만 생성합니다.
시나리오 2: “타입에 따라 다른 객체를 만들어야 해요”
"ShapeType에 따라 Circle, Rectangle, Triangle을 만들어요."
"if (type == CIRCLE) new Circle(); else if... 가 10곳에 흩어져 있어요."
"새 도형 타입 추가할 때마다 여러 파일을 수정해야 해요."
상황: 생성 로직이 분산되어 “어디서 어떤 객체를 만드는지” 파악하기 어렵습니다. 타입 추가 시 누락되기 쉽습니다.
해결 포인트: Factory 패턴으로 생성 로직을 한곳에 모읍니다.
시나리오 3: “데이터가 바뀌면 여러 UI에 알려야 해요”
"주문이 들어오면 이메일, SMS, 푸시 알림을 보내야 해요."
"알림 채널이 늘어날수록 OrderService에 if 분기가 계속 늘어나요."
상황: Subject(주제)가 알릴 대상(Observer)을 직접 알고 있으면, 새 채널 추가 시 Subject를 수정해야 합니다.
해결 포인트: Observer 패턴으로 “알릴 대상”을 인터페이스로 추상화하고 목록으로 관리합니다.
시나리오 4: “데이터 크기에 따라 정렬 알고리즘을 바꿔야 해요”
"100개 미만은 버블 정렬, 1000개 이상은 퀵소트, 10000개 이상은 머지소트를 쓰고 싶어요."
"if (size < 100) bubbleSort(); else if... 로 분기하면 새 알고리즘 추가 시 조건문을 계속 수정해요."
상황: 알고리즘을 런타임에 바꿔야 하는데, if/else 분기로는 확장이 어렵습니다.
해결 포인트: Strategy 패턴으로 알고리즘을 객체로 분리하고 주입합니다.
시나리오 5: “한 줄만 바꿨는데 빌드가 10분 걸려요”
"private 멤버를 하나 추가했는데, 이 클래스를 쓰는 모든 .cpp가 다시 컴파일돼요."
"Boost.Asio를 include하면 50개 파일이 재컴파일돼요."
상황: 헤더에 구현 디테일이 있으면, include하는 모든 파일이 구현 변경에 영향받습니다.
해결 포인트: PIMPL로 구현을 .cpp로 옮겨 컴파일 방화벽을 만듭니다.
패턴별 해결 대상 요약
flowchart TB
subgraph problems["문제 시나리오"]
P1[전역 접근·단일 인스턴스]
P2[타입별 객체 생성]
P3[이벤트 전파·알림]
P4[알고리즘 교체]
P5[빌드 시간·ABI]
end
subgraph patterns["해결 패턴"]
S[Singleton]
F[Factory]
O[Observer]
St[Strategy]
P[PIMPL]
end
P1 --> S
P2 --> F
P3 --> O
P4 --> St
P5 --> P
2. Singleton 패턴
목적
전역에서 단 하나의 인스턴스만 존재하도록 보장하고, 어디서든 그 인스턴스에 접근할 수 있게 합니다. 로거, 설정, DB 연결 풀 등에 사용됩니다.
완전한 예제: 스레드 안전 Logger
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o singleton singleton.cpp && ./singleton
#include <iostream>
#include <string>
#include <mutex>
class Logger {
Logger() = default; // private 생성자
public:
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
// C++11: static 지역 변수 초기화는 스레드 안전
static Logger& getInstance() {
static Logger instance;
return instance;
}
void log(const std::string& message) {
std::lock_guard<std::mutex> lock(mtx_);
std::cout << "[LOG] " << message << "\n";
}
private:
std::mutex mtx_; // 멀티스레드 환경에서 log() 보호
};
int main() {
Logger::getInstance().log("Application started");
Logger::getInstance().log("Config loaded");
return 0;
}
실행 결과:
[LOG] Application started
[LOG] Config loaded
Lazy Initialization (std::call_once)
생성 비용이 큰 객체(DB 연결 등)는 최초 사용 시점에만 초기화하는 것이 좋습니다.
#include <memory>
#include <mutex>
class Database {
static std::unique_ptr<Database> instance_;
static std::once_flag initFlag_;
Database() {
// 무거운 초기화: 연결 풀 생성, 스키마 로드 등
}
public:
static Database& getInstance() {
std::call_once(initFlag_, {
instance_.reset(new Database());
});
return *instance_;
}
Database(const Database&) = delete;
Database& operator=(const Database&) = delete;
};
std::unique_ptr<Database> Database::instance_;
std::once_flag Database::initFlag_;
Singleton 다이어그램
sequenceDiagram participant Client1 participant Client2 participant Logger Client1->>Logger: getInstance() Logger->>Logger: 최초 호출 시 생성 Logger-->>Client1: instance 참조 Client2->>Logger: getInstance() Logger-->>Client2: 동일 instance 참조
3. Factory 패턴
목적
객체 생성 로직을 한곳에 캡슐화하여, 클라이언트가 구체 타입을 알 필요 없이 객체를 얻을 수 있게 합니다. 새 타입 추가 시 Factory만 수정하면 됩니다.
완전한 예제: Shape Factory
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o factory factory.cpp && ./factory
#include <iostream>
#include <memory>
#include <string>
// 제품 인터페이스
class Shape {
public:
virtual ~Shape() = default;
virtual void draw() const = 0;
};
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing Circle\n";
}
};
class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "Drawing Rectangle\n";
}
};
class Triangle : public Shape {
public:
void draw() const override {
std::cout << "Drawing Triangle\n";
}
};
// Factory
enum class ShapeType { Circle, Rectangle, Triangle };
class ShapeFactory {
public:
static std::unique_ptr<Shape> create(ShapeType type) {
switch (type) {
case ShapeType::Circle:
return std::make_unique<Circle>();
case ShapeType::Rectangle:
return std::make_unique<Rectangle>();
case ShapeType::Triangle:
return std::make_unique<Triangle>();
default:
return nullptr;
}
}
};
int main() {
auto circle = ShapeFactory::create(ShapeType::Circle);
auto rect = ShapeFactory::create(ShapeType::Rectangle);
circle->draw(); // Drawing Circle
rect->draw(); // Drawing Rectangle
return 0;
}
실행 결과:
Drawing Circle
Drawing Rectangle
Factory Method (가상 생성)
파생 클래스가 자신이 생성할 구체 타입을 결정하는 패턴입니다.
#include <memory>
class Document {
public:
virtual ~Document() = default;
virtual void open() = 0;
};
class PdfDocument : public Document {
public:
void open() override { /* PDF 열기 */ }
};
class Application {
public:
virtual std::unique_ptr<Document> createDocument() = 0;
void newDocument() {
auto doc = createDocument();
doc->open();
}
};
class PdfApplication : public Application {
public:
std::unique_ptr<Document> createDocument() override {
return std::make_unique<PdfDocument>();
}
};
Factory 다이어그램
flowchart LR
subgraph Client["클라이언트"]
C[create]
end
subgraph Factory["ShapeFactory"]
F[create]
end
subgraph Products["제품"]
P1[Circle]
P2[Rectangle]
P3[Triangle]
end
C --> F
F --> P1
F --> P2
F --> P3
4. Observer 패턴
목적
Subject(주제)가 상태 변경 시 등록된 Observer(관찰자)들에게 자동으로 알림을 보냅니다. Subject는 Observer 구체 타입을 알 필요 없이 인터페이스만 알면 되어 결합도가 낮습니다.
완전한 예제: 주문 알림 시스템
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o observer observer.cpp && ./observer
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <algorithm>
class Observer {
public:
virtual ~Observer() = default;
virtual void update(const std::string& message) = 0;
};
class Subject {
std::vector<std::weak_ptr<Observer>> observers_;
public:
void attach(std::shared_ptr<Observer> observer) {
observers_.push_back(observer);
}
void detach(std::shared_ptr<Observer> observer) {
observers_.erase(
std::remove_if(observers_.begin(), observers_.end(),
[&observer](const std::weak_ptr<Observer>& wp) {
auto sp = wp.lock();
return !sp || sp == observer;
}),
observers_.end()
);
}
void notify(const std::string& message) {
// notify 중 detach 방지: 복사본으로 순회
auto copy = observers_;
for (auto& wp : copy) {
if (auto sp = wp.lock()) {
sp->update(message);
}
}
}
};
// 구체 Observer
class EmailNotifier : public Observer {
public:
void update(const std::string& message) override {
std::cout << "[Email] " << message << "\n";
}
};
class SMSNotifier : public Observer {
public:
void update(const std::string& message) override {
std::cout << "[SMS] " << message << "\n";
}
};
// Subject 확장: OrderService
class OrderService : public Subject {
public:
void placeOrder(const std::string& orderId) {
notify("New order: " + orderId);
}
};
int main() {
OrderService orders;
auto email = std::make_shared<EmailNotifier>();
auto sms = std::make_shared<SMSNotifier>();
orders.attach(email);
orders.attach(sms);
orders.placeOrder("ORD-001");
// [Email] New order: ORD-001
// [SMS] New order: ORD-001
orders.detach(sms);
orders.placeOrder("ORD-002");
// [Email] New order: ORD-002
return 0;
}
실행 결과:
[Email] New order: ORD-001
[SMS] New order: ORD-001
[Email] New order: ORD-002
Observer 다이어그램
sequenceDiagram
participant Client
participant Subject
participant O1 as EmailNotifier
participant O2 as SMSNotifier
Client->>Subject: attach(O1), attach(O2)
Client->>Subject: placeOrder("ORD-001")
Subject->>Subject: notify()
Subject->>O1: update("New order: ORD-001")
Subject->>O2: update("New order: ORD-001")
5. Strategy 패턴
목적
알고리즘을 런타임에 교체할 수 있게 합니다. if/else 분기 대신 Strategy 객체를 주입하면 확장이 쉽고, 테스트 시 mock 전략을 주입하기 쉽습니다.
완전한 예제: 정렬 전략
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o strategy strategy.cpp && ./strategy
#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>
class SortStrategy {
public:
virtual ~SortStrategy() = default;
virtual void sort(std::vector<int>& data) = 0;
};
class BubbleSort : public SortStrategy {
public:
void sort(std::vector<int>& data) override {
std::cout << "BubbleSort\n";
for (size_t i = 0; i < data.size(); ++i) {
for (size_t j = 0; j < data.size() - 1 - i; ++j) {
if (data[j] > data[j + 1]) {
std::swap(data[j], data[j + 1]);
}
}
}
}
};
class QuickSort : public SortStrategy {
void qs(std::vector<int>& data, int left, int right) {
if (left >= right) return;
int pivot = data[(left + right) / 2];
int i = left, j = right;
while (i <= j) {
while (data[i] < pivot) ++i;
while (data[j] > pivot) --j;
if (i <= j) {
std::swap(data[i], data[j]);
++i; --j;
}
}
qs(data, left, j);
qs(data, i, right);
}
public:
void sort(std::vector<int>& data) override {
std::cout << "QuickSort\n";
if (!data.empty()) {
qs(data, 0, static_cast<int>(data.size()) - 1);
}
}
};
// Context
class Sorter {
std::unique_ptr<SortStrategy> strategy_;
public:
void setStrategy(std::unique_ptr<SortStrategy> s) {
strategy_ = std::move(s);
}
void sort(std::vector<int>& data) {
if (strategy_) {
strategy_->sort(data);
}
}
};
int main() {
Sorter sorter;
std::vector<int> data = {5, 2, 8, 1, 9};
sorter.setStrategy(std::make_unique<BubbleSort>());
sorter.sort(data);
data = {5, 2, 8, 1, 9};
sorter.setStrategy(std::make_unique<QuickSort>());
sorter.sort(data);
for (int x : data) std::cout << x << " ";
std::cout << "\n";
return 0;
}
실행 결과:
BubbleSort
QuickSort
1 2 5 8 9
std::function으로 가벼운 Strategy
단순한 전략은 람다로 주입할 수 있습니다.
#include <functional>
#include <vector>
#include <algorithm>
class Sorter {
std::function<void(std::vector<int>&)> strategy_;
public:
void setStrategy(std::function<void(std::vector<int>&)> f) {
strategy_ = std::move(f);
}
void sort(std::vector<int>& data) {
if (strategy_) {
strategy_(data);
}
}
};
// 사용
Sorter sorter;
sorter.setStrategy( {
std::sort(data.begin(), data.end());
});
Strategy 다이어그램
flowchart TB
subgraph Context["Context (Sorter)"]
C[sort]
end
subgraph Strategy["Strategy"]
S[SortStrategy]
end
S --> A[BubbleSort]
S --> B[QuickSort]
S --> C2[MergeSort]
C --> S
6. PIMPL 패턴
목적
구현을 헤더에서 분리하여 컴파일 의존성을 줄이고, ABI 안정성을 확보합니다. “Pointer to Implementation”의 약자로, 컴파일 방화벽(compilation firewall)이라고도 합니다.
완전한 예제: Widget with PIMPL
// widget.h
#include <memory>
class Widget {
public:
Widget();
~Widget(); // unique_ptr<불완전타입> 위해 반드시 선언
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;
void draw();
private:
struct Impl; // 전방 선언만
std::unique_ptr<Impl> pImpl_;
};
// widget.cpp
#include "widget.h"
#include <vector>
#include <string>
#include <iostream>
// 무거운 타입, 외부 라이브러리는 .cpp에만 포함
struct Widget::Impl {
std::vector<int> data_;
std::string name_;
void doHeavyWork() {
// Boost.Asio, OpenSSL 등 여기서만 사용
}
};
Widget::Widget() : pImpl_(std::make_unique<Impl>()) {}
Widget::~Widget() = default; // cpp에 정의 필수
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;
void Widget::draw() {
pImpl_->data_.push_back(1);
std::cout << "Widget drawn, data size: " << pImpl_->data_.size() << "\n";
}
PIMPL 적용 전후 비교
flowchart TB
subgraph before["❌ PIMPL 없음"]
B1[widget.h] --> B2[vector, string, SomeBigLib 전체]
B2 --> B3[include하는 모든 .cpp 재컴파일]
end
subgraph after["✅ PIMPL"]
A1[widget.h] --> A2[pImpl 포인터만]
A2 --> A3[클라이언트 재컴파일 불필요]
A4[widget.cpp] --> A5[struct Impl: 실제 멤버]
end
PIMPL 주의사항
- 소멸자:
unique_ptr<불완전타입>을 사용할 때 소멸자는 반드시 .cpp에 정의해야 합니다. 헤더에= default만 두면 안 됩니다. - 복사:
unique_ptr는 복사 불가이므로, 복사 생성자/대입을 구현하려면Impl을 deep copy해야 합니다. - 이동:
= default로 .cpp에 두면 됩니다.
7. 자주 발생하는 에러와 해결법
Singleton 관련
에러 1: 정적 초기화 순서 fiasco
증상: 다른 전역 객체의 생성자에서 Singleton을 사용할 때, Singleton이 아직 초기화되지 않아 크래시합니다.
원인: 전역 변수 초기화 순서는 정의되지 않습니다.
// ❌ 위험: 전역 Singleton
Config* Config::instance = nullptr; // 다른 전역이 먼저 getInstance() 호출 가능
해결법: 함수 내부 static 지역 변수를 사용합니다. C++11부터 스레드 안전합니다.
// ✅ 안전
static Config& getInstance() {
static Config instance;
return instance;
}
에러 2: Singleton 복사/대입
증상: Config c = Config::getInstance();로 복사하면 인스턴스가 둘로 늘어날 수 있습니다.
해결법: 복사·이동 생성자와 대입 연산자를 = delete로 막습니다.
Config(const Config&) = delete;
Config& operator=(const Config&) = delete;
Factory 관련
에러 3: switch에 default 누락
증상: 새 타입을 enum에 추가했는데 Factory의 switch에 case를 넣지 않아, 잘못된 객체가 반환되거나 nullptr가 반환됩니다.
해결법: [[nodiscard]]와 함께, 알 수 없는 타입일 때 예외를 던지거나 assert합니다.
static std::unique_ptr<Shape> create(ShapeType type) {
switch (type) {
case ShapeType::Circle: return std::make_unique<Circle>();
case ShapeType::Rectangle: return std::make_unique<Rectangle>();
default:
throw std::invalid_argument("Unknown shape type");
}
}
Observer 관련
에러 4: notify 중 Observer가 detach하면 크래시
증상: observer->update() 안에서 subject.detach(this)를 호출하면, 순회 중인 observers 벡터가 수정되어 iterator invalidation으로 크래시합니다.
해결법: notify 시 observers의 복사본을 만들어 순회합니다.
void notify(const std::string& msg) {
auto copy = observers_;
for (auto& wp : copy) {
if (auto sp = wp.lock()) {
sp->update(msg);
}
}
}
에러 5: Observer가 Subject보다 먼저 소멸 (dangling pointer)
증상: raw pointer로 Observer를 저장할 때, Observer가 먼저 파괴되면 Subject가 이미 삭제된 메모리를 참조합니다.
해결법: std::weak_ptr<Observer>를 저장하고, lock()으로 유효할 때만 사용합니다.
Strategy 관련
에러 6: 전략이 nullptr일 때 접근
증상: setStrategy를 호출하지 않고 sort()를 호출하면 strategy->sort(data)에서 크래시합니다.
해결법: nullptr 체크를 하거나, 기본 전략을 설정합니다.
void sort(std::vector<int>& data) {
if (strategy_) {
strategy_->sort(data);
} else {
std::sort(data.begin(), data.end()); // 기본 동작
}
}
PIMPL 관련
에러 7: 소멸자를 헤더에만 두면 링크 에러
증상: ~Widget() = default를 헤더에만 두고, Impl이 불완전 타입인 상태에서 unique_ptr가 삭제를 시도하면 undefined behavior 또는 링크 에러가 발생합니다.
해결법: 소멸자를 .cpp에 정의합니다.
// widget.h
~Widget(); // 선언만
// widget.cpp
Widget::~Widget() = default; // 정의
에러 8: PIMPL 클래스의 복사
증상: Widget w2 = w1; 시 unique_ptr는 복사 불가이므로 컴파일 에러가 납니다.
해결법: 복사가 필요하면 복사 생성자에서 Impl을 deep copy합니다.
Widget::Widget(const Widget& other)
: pImpl_(std::make_unique<Impl>(*other.pImpl_)) {}
8. 모범 사례
Singleton
- 꼭 필요할 때만 사용: 전역 상태는 테스트·병렬화를 어렵게 합니다. 의존성 주입이 가능하면 Singleton 대신 주입을 고려합니다.
- 스레드 안전:
static지역 변수(C++11) 또는std::call_once를 사용합니다. - 초기화 순서: 다른 전역 객체가 Singleton에 의존하지 않도록 설계합니다.
Factory
- 생성 로직 집중: 객체 생성은 Factory 한 곳에서만 수행합니다.
- 스마트 포인터 반환:
std::unique_ptr또는std::shared_ptr로 소유권을 명확히 합니다. - 타입 등록: 런타임에 타입을 등록하는 레지스트리 패턴으로 switch 없이 확장할 수 있습니다.
Observer
- weak_ptr 사용: Observer 수명 관리를 위해
weak_ptr를 저장합니다. - notify 시 복사본 순회: iterator invalidation을 방지합니다.
- 비동기 알림:
update()가 오래 걸리면std::async나 메시지 큐로 비동기 처리합니다.
Strategy
- 무상태 전략: 전략이 내부 상태를 갖지 않으면 여러 Context가 같은 인스턴스를 공유할 수 있습니다.
- std::function 활용: 단순한 전략은 람다로 주입하면 클래스 수가 줄어듭니다.
- 전략 선택 로직 분리: “어떤 전략을 쓸지” 결정하는 코드는 Context 밖(Factory, 설정)에 둡니다.
PIMPL
- Rule of Five: 복사·이동·소멸자를 명시적으로 처리합니다.
- 불완전 타입:
Impl은 헤더에서 전방 선언만 하고, .cpp에서 정의합니다. - ABI 안정성: 라이브러리 배포 시 PIMPL로 내부 레이아웃 변경이 바이너리 호환을 깨지 않게 합니다.
9. 프로덕션 패턴
스레드 안전 Singleton (Meyers’ Singleton)
class Logger {
Logger() = default;
public:
static Logger& getInstance() {
static Logger instance; // C++11: thread-safe lazy init
return instance;
}
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
};
Factory + Strategy 조합
데이터 크기에 따라 정렬 전략을 선택하는 Factory입니다.
std::unique_ptr<SortStrategy> createSortStrategy(size_t dataSize) {
if (dataSize < 100) {
return std::make_unique<BubbleSort>();
} else if (dataSize < 10000) {
return std::make_unique<QuickSort>();
} else {
return std::make_unique<MergeSort>();
}
}
// 사용
Sorter sorter;
sorter.setStrategy(createSortStrategy(data.size()));
sorter.sort(data);
스레드 안전 Observer
#include <mutex>
class ThreadSafeSubject {
std::vector<std::weak_ptr<Observer>> observers_;
mutable std::mutex mtx_;
public:
void attach(std::shared_ptr<Observer> observer) {
std::lock_guard<std::mutex> lock(mtx_);
observers_.push_back(observer);
}
void notify(const std::string& message) {
std::vector<std::shared_ptr<Observer>> copy;
{
std::lock_guard<std::mutex> lock(mtx_);
for (auto& wp : observers_) {
if (auto sp = wp.lock()) {
copy.push_back(sp);
}
}
}
for (auto& obs : copy) {
obs->update(message);
}
}
};
PIMPL + 외부 라이브러리 격리
Boost.Asio, OpenSSL 등 무거운 헤더를 .cpp에만 포함시킵니다.
// network_manager.h
#include <memory>
class NetworkManager {
public:
NetworkManager();
~NetworkManager();
void connect(const std::string& host, int port);
private:
struct Impl;
std::unique_ptr<Impl> pImpl_;
};
// network_manager.cpp
#include "network_manager.h"
#include <boost/asio.hpp> // 여기서만 include
struct NetworkManager::Impl {
boost::asio::io_context io;
boost::asio::ip::tcp::socket socket{io};
// ...
};
10. 패턴 선택 가이드와 체크리스트
패턴 선택 가이드
| 문제 | 추천 패턴 | 대안 |
|---|---|---|
| 전역에서 단일 인스턴스 접근 | Singleton | 의존성 주입 |
| 타입에 따른 객체 생성 | Factory | Builder |
| 상태 변경 시 알림 | Observer | Pub/Sub, 시그널 |
| 알고리즘 런타임 교체 | Strategy | std::function |
| 빌드 시간·ABI 안정성 | PIMPL | 브릿지 |
구현 체크리스트
Singleton
-
static지역 변수 또는std::call_once사용 - 복사·대입
= delete - 멀티스레드 환경에서 log/접근 보호
Factory
- 생성 로직 한 곳에 집중
-
unique_ptr/shared_ptr반환 - 알 수 없는 타입 처리 (예외 또는 assert)
Observer
-
weak_ptr로 Observer 저장 - notify 시 복사본 순회
- detach 시 expired weak_ptr 제거
Strategy
- 전략 nullptr 체크 또는 기본 전략
- 무상태 전략 선호
- 단순한 경우
std::function고려
PIMPL
- 소멸자 .cpp에 정의
- 복사 필요 시 deep copy 구현
- Rule of Five 준수
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 디자인 패턴 | Singleton·Factory·Builder·Prototype 생성 패턴 가이드
- C++ 디자인 패턴 | Adapter·Decorator
- C++ PIMPL과 브릿지 패턴 | 구현 숨기기와 추상화 [#19-3]
- C++ 디자인 패턴 | Observer·Strategy
이 글에서 다루는 키워드 (관련 검색어)
C++ 디자인 패턴, Singleton Factory Observer, Strategy PIMPL, 생성 패턴, 행동 패턴, 구조 패턴 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 패턴 | 용도 | 핵심 원칙 |
|---|---|---|
| Singleton | 단일 인스턴스·전역 접근 | lazy init, 스레드 안전 |
| Factory | 객체 생성 캡슐화 | 생성 로직 집중 |
| Observer | 이벤트 전파 | 느슨한 결합, weak_ptr |
| Strategy | 알고리즘 교체 | 주입, std::function |
| PIMPL | 구현 숨기기 | 컴파일 방화벽, ABI |
핵심 원칙:
- 문제가 있을 때만 패턴 적용
- 인터페이스로 추상화
- 스마트 포인터로 메모리 관리
- 스레드 안전성 고려
자주 묻는 질문 (FAQ)
Q. Singleton과 전역 변수의 차이는?
A: Singleton은 lazy initialization이 가능하고, 생성 시점을 제어할 수 있습니다. 전역 변수는 정적 초기화 순서가 불명확하고, 테스트 시 교체가 어렵습니다. 하지만 Singleton도 전역 상태이므로, 꼭 필요할 때만 사용하는 것이 좋습니다.
Q. Factory와 Builder의 차이는?
A: Factory는 “어떤 타입의 객체를 만들지”에 초점을 맞춥니다. Builder는 “복잡한 객체를 단계적으로 어떻게 만들지”에 초점을 맞춥니다. 생성자 파라미터가 많고 옵션이 다양한 경우 Builder가 유리합니다.
Q. Observer와 Pub/Sub의 차이는?
A: Observer는 Subject가 구독자 목록을 직접 관리합니다. Pub/Sub는 메시지 브로커가 있어 발행자와 구독자가 서로를 모릅니다. 작은 규모에서는 Observer, 대규모에서는 Pub/Sub를 고려합니다.
Q. PIMPL을 쓸 때 성능 오버헤드는?
A: 힙 할당 1회와 포인터 간접 접근 1회가 추가됩니다. 대부분의 경우 무시할 수 있는 수준이며, 빌드 시간 단축과 ABI 안정성 이점이 더 큽니다.
관련 글
- C++ 디자인 패턴 | Observer·Strategy
- C++ 디자인 패턴 | Singleton·Factory·Builder·Prototype 생성 패턴 가이드
- C++ RAII 완벽 가이드 |
- C++ PIMPL과 브릿지 패턴 | 구현 숨기기와 추상화 [#19-3]
- C++ 디자인 패턴 | Adapter·Decorator