콘텐츠로 이동

C++ 생성 패턴 — Factory·Builder·Singleton

생성 패턴(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;
};

패턴의도사용 시점
Factory Method서브클래스가 생성 타입 결정생성할 클래스를 미리 알 수 없을 때
Abstract Factory관련 객체 군 일관 생성제품 패밀리를 교체해야 할 때
Builder단계적 객체 구성매개변수가 많거나 조합이 다양할 때
Singleton인스턴스 1개 보장전역 접근점이 필요할 때 (신중하게)

생성 패턴은 new 호출을 한 곳에 모아 변경에 유연하게 만듭니다. 특히 Abstract Factory는 플랫폼이나 테마 전환 시 클라이언트 코드 수정 없이 객체 군 전체를 교체할 수 있어 강력합니다.