Skip to content

C++ 디자인 패턴 — Singleton·Factory·Observer·Strategy 구현

디자인 패턴(Design Pattern)은 반복되는 소프트웨어 설계 문제에 대한 검증된 해결책 템플릿입니다. GoF(Gang of Four)가 정리한 23가지 패턴 중 C++ 실무와 면접에서 가장 자주 나오는 Singleton, Factory, Observer, Strategy를 C++17 기준으로 구현합니다.

패턴분류핵심 의도
Singleton생성(Creational)인스턴스를 하나만 보장
Factory (Method/Abstract)생성(Creational)객체 생성을 서브클래스에 위임
Observer행동(Behavioral)이벤트 구독/발행
Strategy행동(Behavioral)알고리즘을 교체 가능하게 캡슐화

1. Singleton — 유일한 인스턴스 보장

Section titled “1. Singleton — 유일한 인스턴스 보장”

1.1 기본 구현 (C++11 스레드 안전)

Section titled “1.1 기본 구현 (C++11 스레드 안전)”
class AudioManager
{
public:
// 전역 접근점 — C++11: 정적 지역 변수 초기화는 스레드 안전
static AudioManager& GetInstance()
{
static AudioManager instance; // Magic Static — 딱 한 번 초기화
return instance;
}
void PlaySound(const std::string& id) { /* ... */ }
void StopAll() { /* ... */ }
// 복사·이동 금지
AudioManager(const AudioManager&) = delete;
AudioManager& operator=(const AudioManager&) = delete;
AudioManager(AudioManager&&) = delete;
AudioManager& operator=(AudioManager&&) = delete;
private:
AudioManager() = default; // 외부 생성 금지
~AudioManager() = default;
};
// 사용
AudioManager::GetInstance().PlaySound("explosion");

1.2 CRTP 기반 재사용 가능한 Singleton

Section titled “1.2 CRTP 기반 재사용 가능한 Singleton”
template<typename T>
class Singleton
{
public:
static T& GetInstance()
{
static T instance;
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
protected:
Singleton() = default;
virtual ~Singleton() = default;
};
// 사용
class ConfigManager : public Singleton<ConfigManager>
{
friend class Singleton<ConfigManager>;
ConfigManager()
{
// 설정 파일 로드
}
public:
std::string Get(const std::string& key) const { return config_.at(key); }
void Set(const std::string& key, std::string value) { config_[key] = std::move(value); }
private:
std::unordered_map<std::string, std::string> config_;
};
ConfigManager::GetInstance().Set("MaxPlayers", "16");
장점: 전역 상태를 하나의 객체로 집중 관리
단점: 단위 테스트 어려움 (전역 상태), 과도한 의존성, 소멸 순서 불확실
대안: 의존성 주입(DI)으로 테스트 가능성 개선

2. Factory Pattern — 객체 생성 캡슐화

Section titled “2. Factory Pattern — 객체 생성 캡슐화”
// 추상 제품
struct Shape
{
virtual double Area() const = 0;
virtual std::string Name() const = 0;
virtual ~Shape() = default;
};
// 구체 제품
struct Circle : Shape
{
double radius;
explicit Circle(double r) : radius(r) {}
double Area() const override { return 3.14159 * radius * radius; }
std::string Name() const override { return "Circle"; }
};
struct Rectangle : Shape
{
double width, height;
Rectangle(double w, double h) : width(w), height(h) {}
double Area() const override { return width * height; }
std::string Name() const override { return "Rectangle"; }
};
struct Triangle : Shape
{
double base, height;
Triangle(double b, double h) : base(b), height(h) {}
double Area() const override { return 0.5 * base * height; }
std::string Name() const override { return "Triangle"; }
};

2.2 등록 기반 Factory (확장에 유리)

Section titled “2.2 등록 기반 Factory (확장에 유리)”
class ShapeFactory
{
public:
using Creator = std::function<std::unique_ptr<Shape>()>;
// 타입 등록
static void Register(const std::string& type, Creator creator)
{
GetRegistry()[type] = std::move(creator);
}
// 생성
static std::unique_ptr<Shape> Create(const std::string& type)
{
auto& registry = GetRegistry();
auto it = registry.find(type);
if (it == registry.end())
throw std::invalid_argument("Unknown shape: " + type);
return it->second();
}
private:
static std::unordered_map<std::string, Creator>& GetRegistry()
{
static std::unordered_map<std::string, Creator> registry;
return registry;
}
};
// 등록
ShapeFactory::Register("circle", []{ return std::make_unique<Circle>(1.0); });
ShapeFactory::Register("rectangle", []{ return std::make_unique<Rectangle>(2.0, 3.0); });
// 사용 — 새 도형 추가 시 Factory 코드 수정 불필요
auto shape = ShapeFactory::Create("circle");
std::cout << shape->Name() << ": " << shape->Area() << "\n";

3. Observer Pattern — 이벤트 구독/발행

Section titled “3. Observer Pattern — 이벤트 구독/발행”
#include <functional>
#include <unordered_map>
#include <vector>
// 이벤트 시스템 구현
template<typename... Args>
class Event
{
public:
using HandlerID = uint64_t;
using Handler = std::function<void(Args...)>;
// 구독
HandlerID Subscribe(Handler handler)
{
auto id = ++next_id_;
handlers_[id] = std::move(handler);
return id;
}
// 구독 해제
void Unsubscribe(HandlerID id)
{
handlers_.erase(id);
}
// 발행
void Broadcast(Args... args) const
{
for (const auto& [id, handler] : handlers_)
handler(args...);
}
private:
std::unordered_map<HandlerID, Handler> handlers_;
HandlerID next_id_ = 0;
};
// 사용 예
class Player
{
public:
Event<int> OnHealthChanged; // int: new health
Event<std::string> OnItemPickedUp; // string: item name
void TakeDamage(int damage)
{
health_ -= damage;
OnHealthChanged.Broadcast(health_);
}
void PickUpItem(const std::string& item)
{
OnItemPickedUp.Broadcast(item);
}
private:
int health_ = 100;
};
// 구독자 등록
Player player;
auto hpId = player.OnHealthChanged.Subscribe([](int hp) {
std::cout << "UI 갱신: HP = " << hp << "\n";
});
player.OnHealthChanged.Subscribe([](int hp) {
if (hp <= 20) std::cout << "경고: HP 위험!\n";
});
player.OnItemPickedUp.Subscribe([](const std::string& item) {
std::cout << "인벤토리에 추가: " << item << "\n";
});
player.TakeDamage(30); // UI 갱신: HP = 70
player.TakeDamage(55); // UI 갱신: HP = 15 + 경고 출력
player.PickUpItem(""); // 인벤토리에 추가: 검
player.OnHealthChanged.Unsubscribe(hpId); // 구독 해제

전통적인 Observer 인터페이스 방식

Section titled “전통적인 Observer 인터페이스 방식”
// Observer 인터페이스
class IHealthObserver
{
public:
virtual void OnHealthChanged(int newHealth) = 0;
virtual ~IHealthObserver() = default;
};
// Subject
class HealthComponent
{
public:
void AddObserver(IHealthObserver* observer)
{
observers_.push_back(observer);
}
void RemoveObserver(IHealthObserver* observer)
{
observers_.erase(std::remove(observers_.begin(), observers_.end(), observer),
observers_.end());
}
private:
void NotifyObservers(int health)
{
for (auto* obs : observers_) obs->OnHealthChanged(health);
}
std::vector<IHealthObserver*> observers_;
int health_ = 100;
};

4. Strategy Pattern — 알고리즘 교체 가능하게 캡슐화

Section titled “4. Strategy Pattern — 알고리즘 교체 가능하게 캡슐화”
// 정렬 전략 인터페이스
class ISortStrategy
{
public:
virtual void Sort(std::vector<int>& data) = 0;
virtual std::string Name() const = 0;
virtual ~ISortStrategy() = default;
};
// 구체 전략들
class BubbleSort : public ISortStrategy
{
public:
void Sort(std::vector<int>& data) override
{
for (size_t i = 0; i < data.size(); ++i)
for (size_t j = 0; j + 1 < data.size() - i; ++j)
if (data[j] > data[j+1]) std::swap(data[j], data[j+1]);
}
std::string Name() const override { return "BubbleSort"; }
};
class QuickSort : public ISortStrategy
{
public:
void Sort(std::vector<int>& data) override
{
std::sort(data.begin(), data.end());
}
std::string Name() const override { return "QuickSort"; }
};
// Context — 전략을 사용하는 클래스
class Sorter
{
public:
explicit Sorter(std::unique_ptr<ISortStrategy> strategy)
: strategy_(std::move(strategy)) {}
// 런타임에 전략 교체 가능
void SetStrategy(std::unique_ptr<ISortStrategy> strategy)
{
strategy_ = std::move(strategy);
}
void Sort(std::vector<int>& data)
{
std::cout << strategy_->Name() << " 사용\n";
strategy_->Sort(data);
}
private:
std::unique_ptr<ISortStrategy> strategy_;
};
// 사용
std::vector<int> data = {5, 3, 8, 1, 9, 2};
Sorter sorter(std::make_unique<BubbleSort>());
sorter.Sort(data); // BubbleSort 사용
sorter.SetStrategy(std::make_unique<QuickSort>());
sorter.Sort(data); // QuickSort 사용
// std::function을 이용한 경량 전략
using SortFn = std::function<void(std::vector<int>&)>;
class FlexSorter
{
SortFn strategy_;
public:
explicit FlexSorter(SortFn fn) : strategy_(std::move(fn)) {}
void SetStrategy(SortFn fn) { strategy_ = std::move(fn); }
void Sort(std::vector<int>& data) { strategy_(data); }
};
FlexSorter fs([](std::vector<int>& d) { std::sort(d.begin(), d.end()); });
fs.Sort(data);

상황권장 패턴
전역 상태를 단일 인스턴스로 관리Singleton
생성 로직이 복잡하거나, 타입에 따라 객체 종류 결정Factory
이벤트 발생 시 여러 객체에 통지Observer
동일한 작업을 다양한 알고리즘으로 수행Strategy
런타임 알고리즘 교체 불필요, 정적 다형성 선호CRTP(Template Method)

패턴 = 문제 + 해결책 + 트레이드오프
  • Singleton: Magic Static(C++11)으로 스레드 안전하게 구현. 테스트 어려우므로 DI로 대체 검토.
  • Factory: 등록 기반 팩토리(std::function + map)로 OCP(개방-폐쇄 원칙) 달성.
  • Observer: 함수형 핸들러(std::function)로 인터페이스 없이 경량 구현 가능.
  • Strategy: std::unique_ptr<Interface> 또는 std::function으로 런타임 교체 구현.

모든 패턴은 “변하는 것을 캡슐화하라” 는 원칙의 구체적 적용입니다.