서론
디자인 패턴은 협업하는 코드 구조를 설계할 때, 효율적이고 일관된 방식으로 문제를 해결하는 방법론이다. 특히 객체지향 프로그래밍에서 자주 마주치는 설계 문제들을 다룰 때, 디자인 패턴은 검증된 해결책을 반복적으로 재사용할 수 있는 구조를 제시한다. 패턴을 이해하면 코드의 확장성, 유지보수성, 재사용성을 높이는 데 큰 도움이 되며, 동료 개발자들과의 공통 언어 역할도 해준다. 다만, “패턴을 위한 패턴”은 오히려 해악이 될 수 있다. 따라서 단순히 적용하는 데 그치지 않고, 왜 이 구조가 필요한지, 지금 이 상황에 적절한지를 고민하며 활용하는 태도가 중요하다.
생성 패턴
생성 패턴(Creational Patterns)은 객체를 생성하는 과정을 다루는 디자인 패턴군으로, 어떻게 객체를 만들고, 생성 로직을 구성할 것인지에 초점을 맞춘다. 주요 목적은 객체 생성 과정을 추상화하거나 캡슐화함으로써, 코드의 유연성과 재사용성을 높이고, 결합도를 낮추는 것이다.
게임 개발처럼 복잡한 시스템에서는 단순히 new로 객체를 만드는 것이 아니라, 상황에 따라 다양한 방식으로 객체를 생성하거나, 생성 비용을 줄이고 관리하기 위한 구조가 필요하다. 생성 패턴은 이러한 요구에 맞춰 객체 생성을 통제하고 구조화할 수 있도록 도와준다.
아래와 같이 생성 패턴은 객체 생성을 단순화하고, 생성과 구조를 분리하며, 변경에 강한 코드 구조를 만드는 데 중요한 역할을 한다. 다만, 상황에 맞게 적절한 패턴을 선택하고, 과도한 구조화는 피하는 것이 중요하다.
Abstract Factory(추상 팩토리)
Abstract Factory 패턴은 서로 연관된 객체들을 집합 단위로 생성할 수 있도록 해주는 생성 패턴이다. 즉, 객체를 하나하나 생성하는 것이 아니라, "제품군" 전체를 통일된 방식으로 생성할 수 있는 인터페이스를 제공한다. 이 패턴의 핵심은 객체 생성 책임을 서브클래스에 위임하면서, 클라이언트는 구체 클래스에 의존하지 않도록 만드는 것이다.
게임 개발에서는 이 패턴이 테마나 월드관이 바뀔 때 유용하게 활용된다. 예를 들어, 중세 테마와 SF 테마를 가진 게임이 있다고 하자. 각 테마는 버튼, 체력바, 창 같은 UI 요소가 서로 다르다. Abstract Factory를 사용하면 테마에 맞는 UI 요소들을 일관되게 생성할 수 있어서, 코드 재사용성과 유지보수성이 크게 향상된다.
// 제품 인터페이스들
public interface IButton{
void Render();
}
public interface IHealthBar{
void Render();
}
// 중세 테마 UI 요소
public class MedievalButton : IButton{
public void Render() => Console.WriteLine("중세 스타일 버튼 렌더링");
}
public class MedievalHealthBar : IHealthBar{
public void Render() => Console.WriteLine("중세 스타일 체력바 렌더링");
}
// SF 테마 UI 요소
public class SciFiButton : IButton{
public void Render() => Console.WriteLine("SF 스타일 버튼 렌더링");
}
public class SciFiHealthBar : IHealthBar{
public void Render() => Console.WriteLine("SF 스타일 체력바 렌더링");
}
// 추상 팩토리 인터페이스
public interface IUIFactory{
IButton CreateButton();
IHealthBar CreateHealthBar();
}
// 중세 테마 팩토리
public class MedievalUIFactory : IUIFactory{
public IButton CreateButton() => new MedievalButton();
public IHealthBar CreateHealthBar() => new MedievalHealthBar();
}
// SF 테마 팩토리
public class SciFiUIFactory : IUIFactory{
public IButton CreateButton() => new SciFiButton();
public IHealthBar CreateHealthBar() => new SciFiHealthBar();
}
// 클라이언트 코드
public class UIManager{
private IButton _button;
private IHealthBar _healthBar;
public UIManager(IUIFactory factory){
_button = factory.CreateButton();
_healthBar = factory.CreateHealthBar();
}
public void Render(){
_button.Render();
_healthBar.Render();
}
}
// 사용 예
class Program{
static void Main(){
string theme = "sci-fi"; // 또는 "medieval"
IUIFactory factory = theme == "sci-fi" ? new SciFiUIFactory() : new MedievalUIFactory();
var uiManager = new UIManager(factory);
uiManager.Render();
}
}
이처럼 Abstract Factory는 테마, 시대, 월드 등 전체 제품군을 통제할 수 있는 추상화 계층을 만들어서, 코드의 유연성과 일관성을 높여준다. 새로운 테마가 추가되더라도 팩토리 하나만 추가하면 되고, 클라이언트 코드는 그대로 재사용 가능하다.
다만 단점도 분명하다. 제품군 내부에 새로운 타입(예: 미니맵, 아이콘 등)을 추가하려면 모든 팩토리에 메서드를 추가해야 하며, 테마가 많아질수록 클래스 수가 기하급수적으로 증가한다. 또한 구조적으로 복잡해질 수 있어서, 작은 프로젝트나 간단한 객체 구성에는 오히려 부담이 될 수 있다. 결국 Abstract Factory는 “제품군은 자주 교체되지만, 구성 요소는 자주 바뀌지 않는” 구조에 적합한 패턴이다.
Builder(빌더)
Builder 패턴은 복잡한 객체를 단계별로 나눠서 유연하게 생성할 수 있도록 해주는 생성 패턴이다. 특히 필수 값과 선택 값이 섞여 있고, 생성 순서나 조합이 중요한 상황에서 유용하게 쓰인다. 이 패턴의 핵심은 객체 생성 로직을 분리하여 재사용성과 가독성을 높이는 것이며, 같은 생성 절차로도 서로 다른 형태의 결과물을 만들 수 있도록 도와준다.
게임 개발에서 이 패턴은 캐릭터 생성, 아이템 제작, 옵션 설정 등 조립형 구조를 가진 객체 생성에 자주 활용된다. 예를 들어, 게임 내 전사와 마법사 캐릭터를 생성한다고 할 때, 캐릭터마다 무기와 스킬 구성이 다르지만, 생성 절차는 공통되도록 만들 수 있다. 이때 Builder 패턴을 사용하면 각 캐릭터의 설정을 분리하면서도 전체 흐름은 일관되게 유지할 수 있다.
// 결과물 클래스
public class Character{
public string Name { get; set; }
public string Job { get; set; }
public string Weapon { get; set; }
public List<string> Skills { get; set; } = new List<string>();
public void Show(){
Console.WriteLine($"[{Job}] {Name} - 무기: {Weapon} - 스킬: {string.Join(", ", Skills)}");
}
}
// 빌더 인터페이스
public interface ICharacterBuilder{
void SetName(string name);
void SetJob();
void SetWeapon();
void SetSkills();
Character Build();
}
// 전사 빌더
public class WarriorBuilder : ICharacterBuilder{
private Character _character = new Character();
public void SetName(string name) => _character.Name = name;
public void SetJob() => _character.Job = "전사";
public void SetWeapon() => _character.Weapon = "대검";
public void SetSkills() => _character.Skills.AddRange(new[] { "베기", "방패 막기" });
public Character Build() => _character;
}
// 마법사 빌더
public class MageBuilder : ICharacterBuilder{
private Character _character = new Character();
public void SetName(string name) => _character.Name = name;
public void SetJob() => _character.Job = "마법사";
public void SetWeapon() => _character.Weapon = "지팡이";
public void SetSkills() => _character.Skills.AddRange(new[] { "파이어볼", "텔레포트" });
public Character Build() => _character;
}
// Director 클래스
public class CharacterDirector{
private ICharacterBuilder _builder;
public CharacterDirector(ICharacterBuilder builder){
_builder = builder;
}
public Character Construct(string name){
_builder.SetName(name);
_builder.SetJob();
_builder.SetWeapon();
_builder.SetSkills();
return _builder.Build();
}
}
// 사용 예
class Program{
static void Main(){
var warriorDirector = new CharacterDirector(new WarriorBuilder());
var warrior = warriorDirector.Construct("아르노");
warrior.Show();
var mageDirector = new CharacterDirector(new MageBuilder());
var mage = mageDirector.Construct("엘레나");
mage.Show();
}
}
이처럼 Builder 패턴은 생성 과정이 복잡하거나 순서를 지켜야 하는 객체를 만들 때 매우 유용하다. 또한, 캐릭터 생성 로직이 클래스 내부에서 분리되기 때문에 코드가 단순해지고 유지보수도 쉬워진다.
하지만 단점도 있다. 클래스 수가 많아지고 설계가 다소 복잡해질 수 있으며, 간단한 생성에는 오히려 오버엔지니어링이 될 수 있다. 따라서 생성 단계가 명확하고, 다양한 조합이 필요한 복잡한 객체에 사용할 때 가장 빛을 발하는 패턴이다.
Factory Method(팩토리 메서드)
Factory Method 패턴은 객체를 직접 생성하지 않고, 객체 생성을 서브클래스에 위임함으로써 유연성을 확보하는 생성 패턴이다. 즉, 클라이언트는 어떤 클래스의 객체가 생성되는지 모르거나 알 필요가 없으며, 인터페이스나 추상 클래스만 의존하고 구체적인 생성은 팩토리 메서드에서 처리하게 된다.
게임 개발에서 Factory Method는 몬스터 스폰, 아이템 드롭, 캐릭터 생성 등 종류가 다양하지만 생성 방식은 공통적인 객체를 만들 때 유용하다. 예를 들어, '고블린', '오크', '드래곤' 같은 다양한 몬스터가 있고, 이들을 특정 로직 없이 일관된 방식으로 스폰하고 싶을 때 사용할 수 있다.
// 몬스터 인터페이스
public interface IMonster{
void Attack();
}
// 구체 몬스터 클래스
public class Goblin : IMonster{
public void Attack() => Console.WriteLine("고블린이 창으로 찌른다!");
}
public class Orc : IMonster{
public void Attack() => Console.WriteLine("오크가 도끼로 내리친다!");
}
// 팩토리 추상 클래스
public abstract class MonsterSpawner{
// 팩토리 메서드 (서브클래스에서 생성 책임을 가짐)
public abstract IMonster CreateMonster();
// 공통 로직
public void Spawn(){
IMonster monster = CreateMonster();
Console.WriteLine("몬스터 생성 완료!");
monster.Attack();
}
}
// 구체 팩토리 클래스
public class GoblinSpawner : MonsterSpawner{
public override IMonster CreateMonster() => new Goblin();
}
public class OrcSpawner : MonsterSpawner{
public override IMonster CreateMonster() => new Orc();
}
// 사용 예
class Program{
static void Main(){
MonsterSpawner spawner;
string type = "orc"; // 또는 "goblin"
spawner = type == "orc" ? new OrcSpawner() : new GoblinSpawner();
spawner.Spawn();
}
}
Factory Method 패턴의 핵심은, 객체 생성 로직은 숨기고, 객체의 사용과 생성 책임을 분리해둠으로써 코드 변경 없이 새로운 클래스를 도입할 수 있게 만드는 데 있다. 하지만 단점도 존재한다. 클래스 수가 많아지고 상속 구조가 깊어질 수 있으며, 단순한 객체 생성에는 오히려 복잡도를 높이는 설계가 될 수 있다. 또한, 새로운 객체를 추가할 때마다 팩토리 서브클래스를 추가로 정의해야 하기 때문에 비용이 커질 수 있다.
결론적으로, Factory Method는 “객체 생성을 캡슐화하면서, 하위 클래스에 생성 책임을 넘기고 싶을 때”, 그리고 생성 대상이 다양하지만 처리 방식은 공통적인 상황에서 적합한 패턴이다.
Prototype(프로토타입)
Prototype 패턴은 객체를 새로 생성하는 대신, 기존 객체를 복제(clone)해서 사용하는 생성 패턴이다. 객체를 일일이 new 로 선언하지 않고, 원형 객체(프로토타입)를 복사하여 빠르게 생성할 수 있다. 이 방식은 특히 생성 비용이 비싸거나, 초기 설정이 복잡한 객체를 반복적으로 생성해야 할 때 유용하다.
게임에서는 총알, 몬스터, 스킬 이펙트 등 똑같은 구조를 가진 객체를 빠르게 여러 개 찍어내야 하는 경우가 많다. 이때 Prototype 패턴을 쓰면 매번 새로 초기화하는 과정을 생략하고, 템플릿을 복사하는 방식으로 성능과 구조를 모두 잡을 수 있다.
// 복제 가능한 인터페이스
public interface IUnit : ICloneable{
void Spawn();
}
// 기본 유닛 클래스
public class Monster : IUnit{
public string Name { get; set; }
public int Hp { get; set; }
public string Type { get; set; }
public void Spawn(){
Console.WriteLine($"[{Type}] {Name}가 {Hp} 체력으로 스폰됨!");
}
public object Clone(){
// 얕은 복사로 충분한 경우
return this.MemberwiseClone();
}
}
// 사용 예
class Program{
static void Main(){
// 원형 객체 정의
Monster goblinTemplate = new Monster{
Name = "고블린",
Hp = 100,
Type = "야생"
};
// 복제해서 여러 마리 생성
IUnit g1 = (IUnit)goblinTemplate.Clone();
IUnit g2 = (IUnit)goblinTemplate.Clone();
g1.Spawn();
g2.Spawn();
}
}
이처럼 Prototype 패턴을 사용하면 객체 생성을 단순화하면서도, 원형 객체를 기반으로 빠르게 복사하여 필요한 수만큼 스폰할 수 있다. 객체 초기화 비용이 높은 상황이나, 디자이너가 사전에 구성한 설정을 그대로 재사용하고 싶을 때 유리하다. 단, 복제 과정에서 깊은 복사 vs 얕은 복사를 명확히 구분하지 않으면, 공유된 참조나 버그가 발생할 수 있다. 또한, 객체 내부 상태가 복잡할수록 Clone() 구현이 까다로워질 수 있다. Java, C#처럼 기본 제공되는 Clone()의 동작을 신중하게 재정의해야 하며, 실수할 경우 의도하지 않은 동작이 발생할 수 있다.
결론적으로, Prototype은 “객체를 빠르게 복사하고 싶을 때, 생성비용이나 초기 설정을 아끼고 싶을 때” 매우 유용한 패턴이다. 특히 템플릿 기반 구성과 성능 최적화가 중요한 게임의 유닛, 이펙트, 프리팹 생성 구조에 아주 잘 어울린다.
Singleton(싱글톤)
Singleton 패턴은 어떤 클래스의 인스턴스를 오직 하나만 생성하고, 전역적으로 접근 가능하게 보장하는 생성 패턴이다. 즉, 같은 객체를 여러 곳에서 공유해야 할 때, 어디서든 일관된 인스턴스를 참조할 수 있도록 만든 구조다.
게임 개발에서는 GameManager, AudioManager, InputManager, SaveSystem처럼 전역 상태를 관리하거나, 시스템 전반에 걸쳐 한 번만 초기화되면 되는 객체들이 많다. 이런 경우 Singleton을 사용하면 객체 생성을 제한하고, 전역 접근을 허용함으로써 간결하고 예측 가능한 시스템 구조를 만들 수 있다.
public class GameManager{
// 정적 필드: 유일한 인스턴스
private static GameManager _instance;
// Lock을 통한 멀티스레드 안전 처리 (옵션)
private static readonly object _lock = new object();
// 외부에서 생성하지 못하게 생성자 private
private GameManager(){
Console.WriteLine("GameManager 초기화 완료");
}
// 인스턴스 접근자
public static GameManager Instance{
get{
// 멀티스레드 환경에서도 안전하게 처리
if (_instance == null){
lock (_lock){
if (_instance == null)
_instance = new GameManager();
}
}
return _instance;
}
}
// 예시 기능
public void StartGame(){
Console.WriteLine("게임 시작!");
}
}
// 사용 예
class Program{
static void Main(){
GameManager.Instance.StartGame();
// 어디서든 같은 인스턴스
var another = GameManager.Instance;
Console.WriteLine(ReferenceEquals(another, GameManager.Instance)); // True
}
}
이처럼 Singleton 패턴을 사용하면 GameManager.Instance와 같이 명확하고 일관된 접근 방식으로 전역 객체를 제어할 수 있다. 객체 생성은 한 번만 발생하고, 이후에는 언제든지 같은 인스턴스를 참조할 수 있다. Singleton 패턴의 가장 큰 단점은 전역 상태로 인해 테스트가 어렵고, 객체 간 결합도가 높아져 유연성이 떨어진다는 점이다. 또한 멀티스레드 환경에서 안전하게 구현하지 않으면 예기치 않은 동작이 발생할 수 있고, 남용 시 전역 변수처럼 행동하며 설계의 복잡성과 의존성을 증가시킨다. 따라서 반드시 인스턴스가 하나뿐이어야 하는 명확한 이유가 있을 때에만 사용하는 것이 바람직하다.
결론적으로, Singleton은 "오직 하나만 존재해야 하며, 어디서든 접근할 수 있어야 하는 객체"에 딱 맞는 패턴이다. 하지만 남용하지 말고, 진짜 하나만 있어야 하는 이유가 있을 때 신중히 적용하는 것이 중요하다. 특히 게임 시스템에서는 자주 쓰이지만, 적절한 책임 분리와 테스트 가능성도 함께 고려해야 한다.
참고
디자인 패턴 - 정리
서론 디자인 패턴은 협업하는 코드 구조를 설계할 때, 효율적이고 일관된 방식으로 문제를 해결하는 방법론이다. 특히 객체지향 프로그래밍에서 자주 마주치는 설계 문제들을 다룰 때, 디자인
jinger.tistory.com
디자인 패턴 - 구조 패턴
서론 디자인 패턴은 협업하는 코드 구조를 설계할 때, 효율적이고 일관된 방식으로 문제를 해결하는 방법론이다. 특히 객체지향 프로그래밍에서 자주 마주치는 설계 문제들을 다룰 때, 디자인
jinger.tistory.com
자인 패턴 - 행위 패턴
서론 디자인 패턴은 협업하는 코드 구조를 설계할 때, 효율적이고 일관된 방식으로 문제를 해결하는 방법론이다. 특히 객체지향 프로그래밍에서 자주 마주치는 설계 문제들을 다룰 때, 디자인
jinger.tistory.com
'IT 진로 > 자격증' 카테고리의 다른 글
디자인 패턴 - 행위 패턴 (1) | 2025.06.30 |
---|---|
디자인 패턴 - 구조 패턴 (0) | 2025.06.30 |
ISTQB_CTFL 핵심 정리 (0) | 2024.06.20 |
STQB_CTFL 6. 테스트 지원 도구 (1) | 2024.05.17 |
STQB_CTFL 5. 테스트 관리 (0) | 2024.05.16 |
댓글