C++ 생성 패턴 — Factory·Builder·Singleton·Prototype·Object Pool
생성 패턴 개요
섹션 제목: “생성 패턴 개요”생성 패턴(Creational Pattern)은 객체를 어떻게 만드는가에 관한 패턴입니다. new를 직접 호출하는 대신 생성 로직을 캡슐화해 코드의 유연성과 재사용성을 높입니다.
주요 생성 패턴:
- Factory Method: 서브클래스가 생성할 객체 타입을 결정
- Abstract Factory: 관련 객체 군(family)을 일관되게 생성
- Builder: 복잡한 객체를 단계적으로 구성
- Singleton: 인스턴스를 하나만 보장
Factory Method 패턴
섹션 제목: “Factory Method 패턴”생성할 객체의 타입을 서브클래스에 위임합니다.
// 제품 인터페이스class Enemy{public: virtual ~Enemy() = default; virtual void Attack() = 0; virtual std::string GetName() const = 0;};
// 구체 제품class Zombie : public Enemy{public: void Attack() override { std::cout << "Zombie bites!\n"; } std::string GetName() const override { return "Zombie"; }};
class Vampire : public Enemy{public: void Attack() override { std::cout << "Vampire drains blood!\n"; } std::string GetName() const override { return "Vampire"; }};
// 창조자 (Creator)class EnemySpawner{public: virtual ~EnemySpawner() = default;
// Factory Method virtual std::unique_ptr<Enemy> CreateEnemy() = 0;
// 템플릿 메서드: 생성 + 초기화 로직 std::unique_ptr<Enemy> SpawnEnemy() { auto enemy = CreateEnemy(); std::cout << enemy->GetName() << " spawned!\n"; return enemy; }};
class ZombieSpawner : public EnemySpawner{ std::unique_ptr<Enemy> CreateEnemy() override { return std::make_unique<Zombie>(); }};
class VampireSpawner : public EnemySpawner{ std::unique_ptr<Enemy> CreateEnemy() override { return std::make_unique<Vampire>(); }};
// 사용std::unique_ptr<EnemySpawner> spawner = std::make_unique<ZombieSpawner>();auto enemy = spawner->SpawnEnemy();enemy->Attack();Abstract Factory 패턴
섹션 제목: “Abstract Factory 패턴”관련 객체 군을 함께 생성합니다. 게임 테마(낮/밤) 또는 플랫폼별 UI 컴포넌트에 적합합니다.
// 추상 제품 인터페이스class Weapon { public: virtual void Use() = 0; virtual ~Weapon() = default; };class Armor { public: virtual void Equip() = 0; virtual ~Armor() = default; };
// 판타지 제품군class Sword : public Weapon { public: void Use() override { std::cout << "Slash!\n"; } };class Plate : public Armor { public: void Equip() override { std::cout << "Wear plate armor\n"; } };
// SF 제품군class Laser : public Weapon { public: void Use() override { std::cout << "Pew pew!\n"; } };class Shield : public Armor { public: void Equip() override { std::cout << "Raise shield\n"; } };
// 추상 팩토리class EquipmentFactory{public: virtual ~EquipmentFactory() = default; virtual std::unique_ptr<Weapon> CreateWeapon() = 0; virtual std::unique_ptr<Armor> CreateArmor() = 0;};
class FantasyFactory : public EquipmentFactory{public: std::unique_ptr<Weapon> CreateWeapon() override { return std::make_unique<Sword>(); } std::unique_ptr<Armor> CreateArmor() override { return std::make_unique<Plate>(); }};
class SciFiFactory : public EquipmentFactory{public: std::unique_ptr<Weapon> CreateWeapon() override { return std::make_unique<Laser>(); } std::unique_ptr<Armor> CreateArmor() override { return std::make_unique<Shield>(); }};
// 클라이언트: 팩토리 타입에 무관하게 동작void EquipCharacter(EquipmentFactory& factory){ auto weapon = factory.CreateWeapon(); auto armor = factory.CreateArmor(); weapon->Use(); armor->Equip();}
FantasyFactory fantasy;EquipCharacter(fantasy); // Slash! / Wear plate armorBuilder 패턴
섹션 제목: “Builder 패턴”복잡한 객체를 단계적으로 구성합니다. 생성자 매개변수가 많거나 조합이 다양할 때 사용합니다.
struct CharacterConfig{ std::string name; int hp = 100; int mp = 50; int attackPow = 10; float speed = 1.0f; bool hasMount = false; std::string weapon = "Fist";};
class CharacterBuilder{public: CharacterBuilder& SetName(const std::string& name) { _config.name = name; return *this; // 메서드 체인 }
CharacterBuilder& SetStats(int hp, int mp, int atk) { _config.hp = hp; _config.mp = mp; _config.attackPow = atk; return *this; }
CharacterBuilder& SetSpeed(float speed) { _config.speed = speed; return *this; }
CharacterBuilder& WithMount() { _config.hasMount = true; return *this; }
CharacterBuilder& WithWeapon(const std::string& weapon) { _config.weapon = weapon; return *this; }
CharacterConfig Build() { if (_config.name.empty()) throw std::invalid_argument("Character name is required"); return _config; }
private: CharacterConfig _config;};
// 사용auto hero = CharacterBuilder{} .SetName("Arthur") .SetStats(500, 200, 80) .SetSpeed(1.5f) .WithMount() .WithWeapon("Excalibur") .Build();Director 패턴과 결합
섹션 제목: “Director 패턴과 결합”class CharacterDirector{public: static CharacterConfig BuildWarrior(const std::string& name) { return CharacterBuilder{} .SetName(name) .SetStats(800, 50, 120) .WithWeapon("Great Sword") .Build(); }
static CharacterConfig BuildMage(const std::string& name) { return CharacterBuilder{} .SetName(name) .SetStats(300, 500, 40) .WithWeapon("Staff") .Build(); }};Singleton 패턴
섹션 제목: “Singleton 패턴”인스턴스를 하나만 보장합니다. 게임에서 GameManager, AudioManager, ResourceManager 등에 자주 사용됩니다.
스레드 안전 Singleton (C++11+)
섹션 제목: “스레드 안전 Singleton (C++11+)”class GameManager{public: // Meyers' Singleton: C++11에서 정적 지역 변수 초기화는 스레드 안전 static GameManager& GetInstance() { static GameManager instance; // 최초 호출 시 한 번만 초기화 return instance; }
void Initialize() { _initialized = true; } bool IsInitialized() const { return _initialized; }
// 복사·이동 금지 GameManager(const GameManager&) = delete; GameManager& operator=(const GameManager&) = delete;
private: GameManager() : _initialized(false) {} bool _initialized;};
// 사용GameManager::GetInstance().Initialize();CRTP 기반 재사용 가능한 Singleton
섹션 제목: “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 AudioManager : public Singleton<AudioManager>{ friend class Singleton<AudioManager>; // private 생성자 접근 허용
public: void PlaySound(const std::string& name) { std::cout << "Playing: " << name << "\n"; }
private: AudioManager() = default;};
AudioManager::GetInstance().PlaySound("explosion.wav");Singleton의 단점과 대안
섹션 제목: “Singleton의 단점과 대안”Singleton은 전역 상태를 만들어 테스트 격리가 어렵습니다. 의존성 주입(DI)으로 대체하는 것이 테스트 친화적입니다.
// 나쁜 예: Singleton 직접 의존class Player{ void Die() { AudioManager::GetInstance().PlaySound("die.wav"); // 테스트 불가 }};
// 좋은 예: 인터페이스 주입class IAudioService { public: virtual void PlaySound(const std::string&) = 0; };
class Player{public: explicit Player(IAudioService& audio) : _audio(audio) {}
void Die() { _audio.PlaySound("die.wav"); // 테스트 시 mock 주입 가능 }
private: IAudioService& _audio;};Prototype 패턴
섹션 제목: “Prototype 패턴”기존 객체를 복사(Clone) 해서 새 객체를 생성합니다. 복잡한 초기화 과정 없이 설정된 객체를 빠르게 복제할 때 유용합니다.
// 프로토타입 인터페이스class IEnemy{public: virtual ~IEnemy() = default; virtual std::unique_ptr<IEnemy> Clone() const = 0; virtual void Attack() const = 0; virtual std::string GetType() const = 0;};
// 구체 적 클래스class Goblin : public IEnemy{ int _hp; float _speed; std::string _weaponType;
public: Goblin(int hp, float speed, std::string weapon) : _hp(hp), _speed(speed), _weaponType(std::move(weapon)) {}
// 복사 생성자 기반 Clone std::unique_ptr<IEnemy> Clone() const override { return std::make_unique<Goblin>(*this); // 복사 생성자 호출 }
void Attack() const override { std::cout << "Goblin attacks with " << _weaponType << "! HP=" << _hp << "\n"; } std::string GetType() const override { return "Goblin"; }};
// 프로토타입 레지스트리: 설정된 원형 객체를 저장class EnemyRegistry{ std::unordered_map<std::string, std::unique_ptr<IEnemy>> _prototypes;
public: void Register(const std::string& key, std::unique_ptr<IEnemy> prototype) { _prototypes[key] = std::move(prototype); }
std::unique_ptr<IEnemy> Create(const std::string& key) const { auto it = _prototypes.find(key); if (it == _prototypes.end()) throw std::invalid_argument("Unknown enemy: " + key); return it->second->Clone(); // 원형을 복사해서 반환 }};
// 사용: 설정된 원형을 복사해 대량 생성EnemyRegistry registry;registry.Register("weak_goblin", std::make_unique<Goblin>(50, 2.0f, "dagger"));registry.Register("strong_goblin", std::make_unique<Goblin>(150, 1.5f, "axe"));
// 같은 설정의 적을 빠르게 100개 생성std::vector<std::unique_ptr<IEnemy>> wave;for (int i = 0; i < 50; ++i) wave.push_back(registry.Create("weak_goblin"));for (int i = 0; i < 50; ++i) wave.push_back(registry.Create("strong_goblin"));Object Pool 패턴
섹션 제목: “Object Pool 패턴”객체를 미리 생성해 풀에 보관하고, 필요할 때 빌려쓰고 반납하는 패턴입니다. new/delete 비용이 크거나 GC 압박이 심한 경우에 유용합니다.
template<typename T>class ObjectPool{public: explicit ObjectPool(size_t initialSize) { for (size_t i = 0; i < initialSize; ++i) _pool.push(std::make_unique<T>()); }
// 풀에서 객체를 빌림 (auto-return via custom deleter) std::unique_ptr<T, std::function<void(T*)>> Acquire() { T* obj = nullptr; if (!_pool.empty()) { obj = _pool.top().release(); _pool.pop(); } else { obj = new T(); // 풀 소진 시 새로 생성 }
// 스코프 종료 시 자동으로 풀에 반납 return {obj, [this](T* released) { _pool.push(std::unique_ptr<T>(released)); }}; }
size_t Available() const { return _pool.size(); }
private: std::stack<std::unique_ptr<T>> _pool;};
// 총알 풀 사용 예struct Bullet{ glm::vec3 position; glm::vec3 velocity; float lifetime = 0.f; bool active = false;
void Reset(glm::vec3 pos, glm::vec3 vel) { position = pos; velocity = vel; lifetime = 3.0f; active = true; }};
ObjectPool<Bullet> bulletPool(200); // 총알 200개 미리 생성
void FireGun(glm::vec3 origin, glm::vec3 direction){ auto bullet = bulletPool.Acquire(); bullet->Reset(origin, direction * 50.0f); // 이 bullet이 스코프를 벗어나면 자동으로 풀에 반납 // (실제 게임에서는 활성 총알 목록에 이동)}
// 장점:// 1. new/delete 없이 O(1) 할당/해제// 2. 메모리 단편화 없음// 3. 캐시 친화적 (연속된 메모리에 사전 할당)// 4. Unity/Unreal의 Object Pooling과 동일한 개념| 패턴 | 의도 | 사용 시점 |
|---|---|---|
| Factory Method | 서브클래스가 생성 타입 결정 | 생성할 클래스를 미리 알 수 없을 때 |
| Abstract Factory | 관련 객체 군 일관 생성 | 제품 패밀리를 교체해야 할 때 |
| Builder | 단계적 객체 구성 | 매개변수가 많거나 조합이 다양할 때 |
| Singleton | 인스턴스 1개 보장 | 전역 접근점이 필요할 때 (신중하게) |
| Prototype | 기존 객체 복제 | 복잡한 설정을 가진 객체를 대량 생성할 때 |
| Object Pool | 객체 재사용 | 잦은 생성/소멸로 인한 성능 문제가 있을 때 |
생성 패턴은 new 호출을 한 곳에 모아 변경에 유연하게 만듭니다. 특히 Abstract Factory는 플랫폼이나 테마 전환 시 클라이언트 코드 수정 없이 객체 군 전체를 교체할 수 있어 강력합니다. 게임에서는 총알·파티클 같은 빈번하게 생성/소멸하는 객체에 Object Pool이 필수적으로 사용됩니다.