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");1.3 주의사항
섹션 제목: “1.3 주의사항”장점: 전역 상태를 하나의 객체로 집중 관리단점: 단위 테스트 어려움 (전역 상태), 과도한 의존성, 소멸 순서 불확실대안: 의존성 주입(DI)으로 테스트 가능성 개선2. Factory Pattern — 객체 생성 캡슐화
섹션 제목: “2. Factory Pattern — 객체 생성 캡슐화”2.1 Factory Method
섹션 제목: “2.1 Factory Method”// 추상 제품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 = 70player.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;};
// Subjectclass 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)};6. 흔한 안티패턴 (Anti-Pattern)
섹션 제목: “6. 흔한 안티패턴 (Anti-Pattern)”// 안티패턴 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 Numbersif (player.hp < 20) // 20이 뭔지 알 수 없음 PlayDangerSound();// 해결책: 명명된 상수 사용constexpr int CRITICAL_HP_THRESHOLD = 20;if (player.hp < CRITICAL_HP_THRESHOLD) PlayDangerSound();7. 패턴 선택 가이드
섹션 제목: “7. 패턴 선택 가이드”| 상황 | 권장 패턴 |
|---|---|
| 전역 상태를 단일 인스턴스로 관리 | Singleton |
| 생성 로직이 복잡하거나, 타입에 따라 객체 종류 결정 | Factory |
| 이벤트 발생 시 여러 객체에 통지 | Observer |
| 동일한 작업을 다양한 알고리즘으로 수행 | Strategy |
| 런타임 알고리즘 교체 불필요, 정적 다형성 선호 | CRTP(Template Method) |
8. 정리
섹션 제목: “8. 정리”패턴 = 문제 + 해결책 + 트레이드오프- 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 중 하나 이상의 원칙을 구현하는 구체적 방법이다.
모든 패턴은 “변하는 것을 캡슐화하라” 는 원칙의 구체적 적용입니다.