콘텐츠로 이동

C++ 디자인 패턴

디자인 패턴(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 — 유일한 인스턴스 보장

섹션 제목: “1. Singleton — 유일한 인스턴스 보장”

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

섹션 제목: “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

섹션 제목: “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 — 객체 생성 캡슐화

섹션 제목: “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 (확장에 유리)

섹션 제목: “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 — 이벤트 구독/발행

섹션 제목: “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 인터페이스 방식

섹션 제목: “전통적인 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 — 알고리즘 교체 가능하게 캡슐화

섹션 제목: “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);

5. SOLID 원칙과 디자인 패턴의 관계

섹션 제목: “5. SOLID 원칙과 디자인 패턴의 관계”

디자인 패턴은 SOLID 원칙을 실현하는 구체적인 방법입니다.

S - Single Responsibility Principle (단일 책임 원칙)
"클래스는 하나의 이유로만 변경되어야 한다"
-> Strategy 패턴: 알고리즘을 별도 클래스로 분리
O - Open/Closed Principle (개방-폐쇄 원칙)
"확장에는 열려 있고, 수정에는 닫혀 있어야 한다"
-> Factory 패턴: 새 타입 추가 시 기존 코드 수정 불필요
L - Liskov Substitution Principle (리스코프 치환 원칙)
"서브타입은 기반 타입으로 대체 가능해야 한다"
-> Observer, Strategy에서 인터페이스 기반 다형성 활용
I - Interface Segregation Principle (인터페이스 분리 원칙)
"클라이언트가 사용하지 않는 인터페이스에 의존하지 않아야 한다"
-> 큰 인터페이스 대신 작은 역할별 인터페이스 분리
D - Dependency Inversion Principle (의존성 역전 원칙)
"구체 클래스가 아닌 추상화에 의존해야 한다"
-> Singleton 대신 인터페이스 주입(DI) 활용
// SOLID 위반 예시 (나쁜 코드)
class GameManager
{
public:
void Update()
{
// 여러 책임이 하나의 클래스에 혼재 (SRP 위반)
UpdatePhysics(); // 물리 처리
UpdateAI(); // AI 처리
RenderScene(); // 렌더링
SaveToDatabase(); // 데이터 저장
}
};
// SOLID 준수 예시 (좋은 코드)
class PhysicsSystem { public: virtual void Update(float dt) = 0; };
class AISystem { public: virtual void Update(float dt) = 0; };
class RenderSystem { public: virtual void Render() = 0; };
class PersistSystem { public: virtual void Save() = 0; };
class GameEngine
{
std::vector<std::unique_ptr<PhysicsSystem>> _physicsSystems;
std::vector<std::unique_ptr<AISystem>> _aiSystems;
// 각 시스템은 단일 책임, 인터페이스로 교체 가능 (OCP, DIP)
};

// 안티패턴 1: God Object (만능 클래스)
// 모든 것을 아는 GameManager 클래스 -> 유지보수 불가
class GameManager // 수천 줄의 코드
{
Player* player;
Enemy* enemies[MAX_ENEMIES];
UIManager* ui;
AudioManager* audio;
NetworkManager* network;
// ... 모든 게임 서브시스템을 직접 참조
void DoEverything() { /* ... */ }
};
// 해결책: 시스템을 독립 클래스로 분리, 이벤트/메시지로 통신
// 안티패턴 2: Singleton 남용
// 모든 것을 Singleton으로 만들면 전역 상태로 테스트 불가
InputManager::GetInstance().IsKeyDown(Key::A);
AudioManager::GetInstance().PlaySound("x");
NetworkManager::GetInstance().Send(packet);
// -> 단위 테스트 시 모든 Singleton 초기화 필요, 테스트 간 상태 오염
// 해결책: 필요한 의존성을 생성자 주입으로 전달
// 안티패턴 3: Primitive Obsession (기본 타입 남용)
void MovePlayer(float x, float y, float z, float speed, float yaw, float pitch)
// -> 매개변수 순서 혼동, 가독성 저하
// 해결책: 의미 있는 구조체/클래스로 래핑
struct Position { float x, y, z; };
struct Rotation { float yaw, pitch; };
void MovePlayer(Position pos, float speed, Rotation rot);
// 안티패턴 4: Magic Numbers
if (player.hp < 20) // 20이 뭔지 알 수 없음
PlayDangerSound();
// 해결책: 명명된 상수 사용
constexpr int CRITICAL_HP_THRESHOLD = 20;
if (player.hp < CRITICAL_HP_THRESHOLD)
PlayDangerSound();

상황권장 패턴
전역 상태를 단일 인스턴스로 관리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으로 런타임 교체 구현.
  • SOLID 원칙: 각 패턴은 S/O/L/I/D 중 하나 이상의 원칙을 구현하는 구체적 방법이다.

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