콘텐츠로 이동

C++ 생성 패턴 — Factory·Builder·Singleton·Prototype·Object Pool

생성 패턴(Creational Pattern)은 객체를 어떻게 만드는가에 관한 패턴입니다. new를 직접 호출하는 대신 생성 로직을 캡슐화해 코드의 유연성과 재사용성을 높입니다.

주요 생성 패턴:

  • Factory Method: 서브클래스가 생성할 객체 타입을 결정
  • Abstract Factory: 관련 객체 군(family)을 일관되게 생성
  • Builder: 복잡한 객체를 단계적으로 구성
  • Singleton: 인스턴스를 하나만 보장

생성할 객체의 타입을 서브클래스에 위임합니다.

// 제품 인터페이스
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();

관련 객체 군을 함께 생성합니다. 게임 테마(낮/밤) 또는 플랫폼별 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 armor

복잡한 객체를 단계적으로 구성합니다. 생성자 매개변수가 많거나 조합이 다양할 때 사용합니다.

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();
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();
}
};

인스턴스를 하나만 보장합니다. 게임에서 GameManager, AudioManager, ResourceManager 등에 자주 사용됩니다.

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();
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은 전역 상태를 만들어 테스트 격리가 어렵습니다. 의존성 주입(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;
};

기존 객체를 복사(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"));

객체를 미리 생성해 풀에 보관하고, 필요할 때 빌려쓰고 반납하는 패턴입니다. 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이 필수적으로 사용됩니다.