서론
디자인 패턴은 협업하는 코드 구조를 설계할 때, 효율적이고 일관된 방식으로 문제를 해결하는 방법론이다. 특히 객체지향 프로그래밍에서 자주 마주치는 설계 문제들을 다룰 때, 디자인 패턴은 검증된 해결책을 반복적으로 재사용할 수 있는 구조를 제시한다. 패턴을 이해하면 코드의 확장성, 유지보수성, 재사용성을 높이는 데 큰 도움이 되며, 동료 개발자들과의 공통 언어 역할도 해준다. 다만, “패턴을 위한 패턴”은 오히려 해악이 될 수 있다. 따라서 단순히 적용하는 데 그치지 않고, 왜 이 구조가 필요한지, 지금 이 상황에 적절한지를 고민하며 활용하는 태도가 중요하다.
행위 패턴
행위 패턴(Behavioral Patterns)은 객체 간의 책임 분산, 소통 방식, 알고리즘 구조에 초점을 맞춘 디자인 패턴이다. 즉, "무엇을" 할지는 객체가 갖고 있고, "어떻게 협력할지"를 설계하는 데 도움을 주는 패턴들이다. 복잡한 로직을 여러 객체가 효율적으로 분담하거나, 메시지를 주고받는 구조를 설계할 때 사용된다. 이러한 패턴들은 게임 개발에서도 광범위하게 활용된다. 예를 들어 Observer는 이벤트 시스템, Command는 입력 매핑 시스템, Strategy는 AI 행동 선택, State는 캐릭터 상태 전환에 자주 사용된다. 정리하자면, 행위 패턴은 단순히 기능을 만들기보다 협력 관계와 책임 분배를 명확히 해주는 도구들이며, 유지보수성과 유연성을 갖춘 행동 구조의 설계 틀이라고 할 수 있다.
Chain of Responsibility(책임 연쇄)
Chain of Responsibility 패턴은 하나의 요청을 여러 객체가 순차적으로 처리할 수 있도록 연결된 구조를 만드는 행위 패턴이다. 즉, 요청을 보낸 쪽은 누가 처리하는지 알 필요가 없고, 처리 가능한 객체가 나올 때까지 다음 객체로 책임을 넘긴다. 이 덕분에 요청 발신자와 수신자의 결합도가 낮아지고, 유연한 처리 흐름을 만들 수 있다.
게임에서는 이 패턴이 입력 처리 체인, 충돌 처리 체인, 이벤트 필터 체인, UI 이벤트 분기 등에 자주 쓰인다. 예를 들어, 키보드 입력이 왔을 때 현재 UI → 게임 상태 → 디버그 모드 순으로 전달하여, 가장 먼저 처리할 수 있는 대상이 응답하는 식이다.
// 추상 핸들러
public abstract class GameHandler{
protected GameHandler _next;
public void SetNext(GameHandler next){
_next = next;
}
public abstract void Handle(string request);
}
// 구체 핸들러 1: UI
public class UIHandler : GameHandler{
public override void Handle(string request){
if (request == "ESC"){
Console.WriteLine("UIHandler: ESC 입력 -> 메뉴 열기");
}
else{
_next?.Handle(request);
}
}
}
// 구체 핸들러 2: Game State
public class GameStateHandler : GameHandler{
public override void Handle(string request){
if (request == "W"){
Console.WriteLine("GameStateHandler: 캐릭터 앞으로 이동");
}
else{
_next?.Handle(request);
}
}
}
// 구체 핸들러 3: Debug
public class DebugHandler : GameHandler{
public override void Handle(string request){
if (request == "F12"){
Console.WriteLine("DebugHandler: 디버그 콘솔 열기");
}
else{
Console.WriteLine("요청 처리 불가: " + request);
}
}
}
// 사용 예
class Program{
static void Main(){
var ui = new UIHandler();
var game = new GameStateHandler();
var debug = new DebugHandler();
ui.SetNext(game);
game.SetNext(debug);
string[] inputs = { "ESC", "W", "F12", "TAB" };
foreach (var input in inputs){
Console.WriteLine($"입력 처리: {input}");
ui.Handle(input);
Console.WriteLine();
}
}
}
이처럼 체인이 구성되면 클라이언트는 어디서 처리되는지 몰라도 되고, 핸들러들은 각자 처리 가능한 요청만 응답하며, 나머지는 다음으로 넘긴다. 단점은 체인 구성 순서에 따라 결과가 달라질 수 있고, 잘못하면 요청이 끝까지 전달돼도 처리되지 않는 일이 생긴다는 점이다. 또한 너무 많은 핸들러가 연결되면 디버깅이나 흐름 추적이 어려워질 수 있다.
결론적으로 Chain of Responsibility는 여러 객체가 협력하여 책임을 나눠야 할 때 유용하며, 특히 입력 처리, 상태 관리, 이벤트 전달 흐름 같은 순차적 검토 구조에 적합한 패턴이다.
Command(커맨드)
Command 패턴은 요청을 객체로 캡슐화해서 호출자와 실행자의 결합을 분리하는 행위 패턴이다. 즉, 실행하고 싶은 동작을 Command 객체로 표현함으로써, 명령을 저장, 취소, 큐잉, 로그로 기록할 수 있게 만든다. 클라이언트는 어떤 명령이든 Execute()만 호출하면 되고, 내부 구현은 몰라도 된다.
게임에서는 이 패턴이 입력 처리 시스템, 유저 행동 기록/되돌리기(Undo), 매크로 재생, 튜토리얼 자동 조작 등에서 유용하게 쓰인다. 예를 들어 사용자가 "공격", "방어", "이동"을 입력할 때, 각 동작을 커맨드 객체로 만들어 저장해두면, 이를 나중에 재실행하거나 되돌리기도 가능하다.
// Receiver: 실제 기능을 수행하는 클래스
public class Player{
public void Move() => Console.WriteLine("플레이어가 이동합니다.");
public void Attack() => Console.WriteLine("플레이어가 공격합니다.");
}
// Command 인터페이스
public interface ICommand{
void Execute();
}
// 구체 커맨드들
public class MoveCommand : ICommand{
private Player _player;
public MoveCommand(Player player) => _player = player;
public void Execute() => _player.Move();
}
public class AttackCommand : ICommand{
private Player _player;
public AttackCommand(Player player) => _player = player;
public void Execute() => _player.Attack();
}
// Invoker: 커맨드를 받아 실행하는 주체
public class InputHandler{
private Dictionary<string, ICommand> _commandMap = new Dictionary<string, ICommand>();
public void Bind(string key, ICommand command){
_commandMap[key] = command;
}
public void HandleInput(string key){
if (_commandMap.TryGetValue(key, out var command))
command.Execute();
else
Console.WriteLine("알 수 없는 입력: " + key);
}
}
// 사용 예
class Program{
static void Main(){
var player = new Player();
var move = new MoveCommand(player);
var attack = new AttackCommand(player);
var input = new InputHandler();
input.Bind("W", move);
input.Bind("SPACE", attack);
input.HandleInput("W"); // 이동
input.HandleInput("SPACE"); // 공격
}
}
이 구조를 사용하면 입력 키와 실행 로직이 분리되어, 키맵 변경이나 되돌리기, 로그 기록도 쉽게 가능하다. Command 패턴의 단점은 클래스 수가 많아지고 구조가 복잡해질 수 있다는 점이다. 단순한 동작에도 매번 커맨드 클래스를 만들어야 하므로, 소규모 프로젝트에는 오히려 과한 설계가 될 수 있다. 또한 명령의 Undo/Redo까지 지원하려면 상태 저장이나 Memento 패턴과 결합해야 하므로 구현 난이도도 올라간다.
결론적으로 Command는 행동을 객체로 추상화하고자 할 때 가장 유용한 패턴이다. 특히 사용자 행동 저장, 입력 큐잉, 자동 실행 같은 기능을 설계할 땐 Command 패턴이 강력한 선택지가 된다.
Interpreter(인터프리터)
Interpreter 패턴은 특정 도메인의 언어나 표현식을 해석할 수 있도록 클래스로 문법을 표현하고, 해석기를 구현하는 행위 패턴이다. 쉽게 말해, "작은 언어(Domain-Specific Language)"를 만들고 그것을 해석하는 구조를 객체지향적으로 설계하는 방식이다. 이 패턴은 수학식 계산기, 게임 내 명령어 처리, 간단한 AI 스크립트, 대사 스크립트 해석 등에 활용된다.
게임에서는 예를 들어, NPC의 행동을 스크립트로 작성하거나, 퀘스트 조건을 간단한 문법으로 표현한 후 자동 해석하게 만들 수 있다. 이런 식의 스크립트를 클래스 계층 구조로 정의하고, 해석기(Interpreter)를 통해 실행하는 방식이다.
// 표현식 인터페이스
public interface IExpression{
bool Interpret(Dictionary<string, bool> context);
}
// 터미널 표현식
public class TerminalExpression : IExpression{
private string _variable;
public TerminalExpression(string variable){
_variable = variable;
}
public bool Interpret(Dictionary<string, bool> context){
return context.ContainsKey(_variable) && context[_variable];
}
}
// OR 표현식
public class OrExpression : IExpression{
private IExpression _expr1;
private IExpression _expr2;
public OrExpression(IExpression expr1, IExpression expr2){
_expr1 = expr1;
_expr2 = expr2;
}
public bool Interpret(Dictionary<string, bool> context){
return _expr1.Interpret(context) || _expr2.Interpret(context);
}
}
// AND 표현식
public class AndExpression : IExpression{
private IExpression _expr1;
private IExpression _expr2;
public AndExpression(IExpression expr1, IExpression expr2){
_expr1 = expr1;
_expr2 = expr2;
}
public bool Interpret(Dictionary<string, bool> context){
return _expr1.Interpret(context) && _expr2.Interpret(context);
}
}
// 사용 예
class Program
{
static void Main(){
// 퀘스트 조건: "hasKey AND hasSword"
IExpression hasKey = new TerminalExpression("hasKey");
IExpression hasSword = new TerminalExpression("hasSword");
IExpression condition = new AndExpression(hasKey, hasSword);
// 현재 상태
var context = new Dictionary<string, bool>{
{ "hasKey", true },
{ "hasSword", false }
};
Console.WriteLine("퀘스트 조건 충족? " + condition.Interpret(context)); // false
}
}
이 예시에서 Interpret 함수는 문맥(Context)에 따라 해석 결과를 반환한다. 즉, "hasKey AND hasSword"라는 조건은 현재 상태를 해석해서 퀘스트 수락 가능 여부를 판단하는 식이다. 단점은 문법이 복잡하거나 문장이 길어질수록 클래스가 급증하고 유지보수가 어려워진다는 점이다. 또한 실행 속도도 느릴 수 있고, 새로운 문법을 추가할 때마다 해석기 구조를 수정해야 하므로 유연성도 떨어진다.
결론적으로 Interpreter는 단순한 규칙 기반 스크립트나 DSL을 해석하는 데 유용하지만, 복잡한 언어라면 직접 구현하기보다는 파서(generator)를 사용하거나 다른 방식(예: Expression Tree + Eval 구조)을 고려하는 게 좋다. 게임에서는 간단한 조건 해석, 튜토리얼 대사 트리거, 퀘스트 조건 분기에 잘 어울린다.
Iterator(이터레이터)
Iterator 패턴은 컬렉션(리스트, 배열 등)의 내부 구조를 노출하지 않고, 요소들을 순차적으로 접근할 수 있도록 해주는 패턴이다. 즉, "어떻게 저장되어 있는지"는 몰라도, 하나씩 꺼내면서 반복 처리할 수 있도록 만든다. 이 덕분에 반복 로직을 캡슐화하고, 컬렉션 구조와 반복 방식의 결합도를 낮출 수 있다.
게임 개발에서는 예를 들어 NPC 그룹을 순회하면서 AI 동작 처리, UI 컴포넌트를 순차적으로 업데이트, 플레이어 인벤토리나 장비 목록 탐색, 웨이포인트 리스트 순회 등에 자주 사용된다.
// 게임 유닛 클래스
public class GameUnit{
public string Name { get; set; }
public GameUnit(string name) => Name = name;
public void Act() => Console.WriteLine($"{Name} 행동 시작!");
}
// 컬렉션 인터페이스
public interface IUnitGroup{
IUnitIterator CreateIterator();
}
// 이터레이터 인터페이스
public interface IUnitIterator{
bool HasNext();
GameUnit Next();
}
// 구체 컬렉션
public class UnitGroup : IUnitGroup{
private List<GameUnit> _units = new List<GameUnit>();
public void AddUnit(GameUnit unit) => _units.Add(unit);
public IUnitIterator CreateIterator() => new UnitIterator(_units);
}
// 구체 이터레이터
public class UnitIterator : IUnitIterator{
private List<GameUnit> _units;
private int _position = 0;
public UnitIterator(List<GameUnit> units) => _units = units;
public bool HasNext() => _position < _units.Count;
public GameUnit Next() => _units[_position++];
}
// 사용 예
class Program{
static void Main(){
var group = new UnitGroup();
group.AddUnit(new GameUnit("기사"));
group.AddUnit(new GameUnit("궁수"));
group.AddUnit(new GameUnit("마법사"));
var iterator = group.CreateIterator();
while (iterator.HasNext()){
var unit = iterator.Next();
unit.Act();
}
}
}
이 구조를 사용하면 UnitGroup의 내부가 어떻게 구성되어 있는지 몰라도, Iterator만으로 모든 유닛을 순회하면서 행동시킬 수 있다. Iterator의 장점은 구조 은닉과 반복 기능 분리로, 컬렉션 구조가 변경되더라도 반복 코드는 그대로 유지될 수 있다는 점이다. 반면에 단점은 단순 순회 외에는 부가 기능이 부족하고, 이미 대부분 언어(C#, Java, Python 등)에서 기본 이터레이터/foreach가 내장되어 있기 때문에 별도로 구현하는 일이 적다는 점이다. 즉, 직접 구현할 일은 많지 않지만, 직접 설계한 컬렉션을 사용자 정의 반복 방식으로 순회하고 싶을 때는 여전히 유용한 패턴이다. 게임에선 특히 객체 풀 순회, 상태 갱신 루프, 타겟 검색 등에 자연스럽게 적용된다.
Mediator(미디에이터)
Mediator 패턴은 객체 간 복잡한 상호작용을 중재자(중앙 허브) 객체로 캡슐화해서 서로 직접 통신하지 않고, 중앙을 통해 간접적으로 소통하도록 만드는 행위 패턴이다. 즉, 여러 객체가 서로 알아야 할 필요 없이 중재자만 알고 있으면 상호작용이 가능하므로, 결합도를 낮추고 객체 간 독립성을 확보할 수 있다.
게임에서는 UI 요소 간 상호작용 조율, 캐릭터 간 협업 로직, 네트워크 룸 채팅 메시지 중계, 이벤트 브로커 시스템 등에 유용하게 쓰인다. 예를 들어, 버튼을 누르면 여러 UI 요소가 함께 반응해야 할 때, 각 요소가 서로 직접 접근하는 대신, UIManager 같은 중재자에게 알리고, 중재자가 필요한 UI에 명령을 전달하는 방식이다.
// Mediator 인터페이스
public interface IUIManager{
void Notify(UIComponent sender, string eventCode);
}
// 추상 컴포넌트
public abstract class UIComponent{
protected IUIManager _mediator;
public UIComponent(IUIManager mediator){
_mediator = mediator;
}
}
// 버튼 컴포넌트
public class UIButton : UIComponent{
public UIButton(IUIManager mediator) : base(mediator) {}
public void Click(){
Console.WriteLine("버튼 클릭됨");
_mediator.Notify(this, "ButtonClicked");
}
}
// 팝업창 컴포넌트
public class UIPopup : UIComponent{
public UIPopup(IUIManager mediator) : base(mediator) {}
public void Show(){
Console.WriteLine("팝업창 표시");
}
public void Hide(){
Console.WriteLine("팝업창 숨김");
}
}
// UI 매니저 (Mediator 구현)
public class UIManager : IUIManager{
public UIButton Button { get; set; }
public UIPopup Popup { get; set; }
public void Notify(UIComponent sender, string eventCode){
if (eventCode == "ButtonClicked"){
Popup.Show();
}
}
}
// 사용 예
class Program{
static void Main(){
var uiManager = new UIManager();
var button = new UIButton(uiManager);
var popup = new UIPopup(uiManager);
uiManager.Button = button;
uiManager.Popup = popup;
button.Click(); // 버튼 누르면 -> 팝업창 표시
}
}
이 구조에서 UIButton과 UIPopup은 서로를 전혀 모르지만, UIManager를 통해 상호작용이 이루어진다. 컴포넌트가 많아질수록 결합도 문제 없이 중재자만 조정하면 되기 때문에 유지보수가 훨씬 쉬워진다. 단점은 모든 로직이 Mediator에 집중되면 중재자가 과도하게 비대해져 관리가 어려워진다는 점이다. 즉, 중재자는 복잡한 통신을 단순화해주지만, 결국 모든 의사결정의 중심이 되므로 코드가 커지고 집중화되는 부작용이 생길 수 있다.
결론적으로 Mediator는 많은 객체가 서로 엮여야 하는 시스템에서 결합도를 낮추고 독립성을 확보할 수 있는 강력한 구조다. 특히 게임에서는 UI, 이벤트, 멀티플레이 채팅 등 메시지 중심 시스템에 자주 활용된다.
Memento(메멘토)
Memento 패턴은 객체의 내부 상태를 캡슐화하여 외부에서 저장하고, 필요할 때 복원할 수 있게 하는 행위 패턴이다. 즉, 객체의 상태를 일시적으로 저장해뒀다가 나중에 되돌릴 수 있는 구조다.
Undo(실행 취소), Save/Load 시스템, 시간 되돌리기 같은 기능에서 매우 유용하며, 게임에서는 체크포인트, 세이브 슬롯, 캐릭터 성장 상태 복구, 전략 게임의 턴 되돌리기 등에 자주 쓰인다.
// 저장할 상태 객체
public class GameState{
public int PlayerHP { get; set; }
public int Coins { get; set; }
public GameState Clone(){
return (GameState)this.MemberwiseClone();
}
public override string ToString(){
return $"HP: {PlayerHP}, Coins: {Coins}";
}
}
// Memento
public class GameMemento{
private GameState _state;
public GameMemento(GameState state){
_state = state.Clone(); // 깊은 복사
}
public GameState GetSavedState(){
return _state.Clone();
}
}
// Originator (상태를 가지는 객체)
public class Game{
public GameState State { get; private set; } = new GameState();
public void Play(int damage, int earnedCoins){
State.PlayerHP -= damage;
State.Coins += earnedCoins;
Console.WriteLine("현재 상태: " + State);
}
public GameMemento Save() => new GameMemento(State);
public void Load(GameMemento memento) => State = memento.GetSavedState();
}
// Caretaker (상태를 저장해두는 관리자)
public class SaveSystem{
private Stack<GameMemento> _history = new Stack<GameMemento>();
public void Save(Game game) => _history.Push(game.Save());
public void Undo(Game game){
if (_history.Count > 0)
game.Load(_history.Pop());
}
}
// 사용 예
class Program{
static void Main(){
var game = new Game();
var saves = new SaveSystem();
game.State.PlayerHP = 100;
game.State.Coins = 0;
saves.Save(game); // 초기 상태 저장
game.Play(20, 10); // 데미지 20, 코인 +10
saves.Save(game);
game.Play(50, 5); // 더 큰 피해
Console.WriteLine("되돌리기 실행");
saves.Undo(game); // 한 단계 전으로 복원
Console.WriteLine("복원된 상태: " + game.State);
}
}
이 구조에서 Game은 내부 상태를 Memento로 저장하고 SaveSystem이 이를 관리한다. 복원할 때는 Load()만 호출하면 된다. 객체의 캡슐화를 유지한 채로 상태 저장과 복원이 가능하다는 게 핵심이다. 단점은 상태가 복잡하거나 자주 저장되면 메모리 사용량이 급격히 늘 수 있다는 점이다. 또한 객체의 깊은 복사 구현이 잘못되면 정확히 복원되지 않거나 부작용이 생길 수 있다. 즉, 상태의 크기와 빈도에 따라 적절한 저장 전략을 함께 설계해야 한다.
요약하자면 Memento는 되돌리기/세이브 기능을 구조적으로 안전하게 구현할 수 있게 도와주는 패턴으로, 게임 시스템에서는 시간 되돌리기, 플레이어 상태 저장, 진행도 저장 등에 적합한 강력한 도구다.
Observer(옵저버)
Observer 패턴은 어떤 객체의 상태 변화가 있을 때, 그 변화에 관심 있는 다른 객체들에게 자동으로 알림을 보내는 구조다. 즉, 한 객체의 이벤트를 여러 객체가 구독(subscribe)하고, 상태가 바뀌면 자동으로 통보(notify)받아 동작한다.
게임 개발에서는 이벤트 시스템, UI 갱신, 퀘스트 트리거, AI 반응, 업적 시스템 등에서 매우 많이 사용된다.
// Subject (관찰 대상)
public class Enemy{
private List<IEnemyObserver> _observers = new List<IEnemyObserver>();
public int HP { get; private set; } = 100;
public void Attach(IEnemyObserver observer) => _observers.Add(observer);
public void Detach(IEnemyObserver observer) => _observers.Remove(observer);
public void TakeDamage(int damage){
HP -= damage;
Console.WriteLine($"[Enemy] 피해 받음: {damage}, 남은 HP: {HP}");
Notify();
}
private void Notify(){
foreach (var obs in _observers)
obs.OnEnemyDamaged(HP);
}
}
// Observer 인터페이스
public interface IEnemyObserver{
void OnEnemyDamaged(int newHP);
}
// 관찰자 1: UI 체력바
public class HPBar : IEnemyObserver{
public void OnEnemyDamaged(int newHP){
Console.WriteLine($"[HPBar] 체력바 갱신: {newHP}");
}
}
// 관찰자 2: AI 백업 요청
public class AlertSystem : IEnemyObserver{
public void OnEnemyDamaged(int newHP){
if (newHP < 30)
Console.WriteLine("[Alert] 적 체력 낮음, 지원 요청!");
}
}
// 사용 예
class Program{
static void Main(){
var enemy = new Enemy();
var hpBar = new HPBar();
var alert = new AlertSystem();
enemy.Attach(hpBar);
enemy.Attach(alert);
enemy.TakeDamage(20); // HP 80
enemy.TakeDamage(40); // HP 40
enemy.TakeDamage(15); // HP 25 → 알림 발생
}
}
이 구조를 사용하면 Enemy의 상태가 바뀔 때마다 HPBar, AlertSystem 등 여러 모듈이 자동으로 반응한다. 상호 결합 없이 유연하게 반응형 구조를 만들 수 있다는 게 Observer 패턴의 핵심 장점이다. 반면 단점은 구독자가 많아지면 호출 비용이 커지고, 디버깅 시 어디서 반응했는지 추적이 어려울 수 있다. 또한, 구독 해제 누락 시 메모리 누수가 발생하거나, 순환 참조 문제가 생길 수도 있다.
정리하자면 Observer는 상태 변화에 따른 반응형 이벤트 시스템을 설계할 때 가장 직관적이고 강력한 패턴이다. 게임에서는 특히 UI, 이벤트, 실시간 반응형 시스템에서 매우 널리 활용된다.
State(상태)
State 패턴은 객체의 내부 상태에 따라 행동이 달라지도록 하고, 상태 전환을 클래스화해서 관리하는 구조다. 즉, 상태별 로직을 별도 클래스로 분리해두고, 현재 상태 객체에 따라 다르게 동작하도록 만든다. 게임에서는 캐릭터의 행동 상태(대기, 공격, 피격, 사망), 몬스터 AI 상태, 튜토리얼 진행 단계, UI 전환 상태 등 다양한 곳에서 유용하게 쓰인다.
// 상태 인터페이스
public interface ICharacterState{
void Handle(Character character);
}
// 캐릭터 상태 1: 대기 상태
public class IdleState : ICharacterState{
public void Handle(Character character){
Console.WriteLine("캐릭터가 대기 중입니다.");
// 조건에 따라 상태 전환
character.SetState(new AttackState());
}
}
// 캐릭터 상태 2: 공격 상태
public class AttackState : ICharacterState{
public void Handle(Character character){
Console.WriteLine("캐릭터가 공격 중입니다.");
character.SetState(new DeadState());
}
}
// 캐릭터 상태 3: 사망 상태
public class DeadState : ICharacterState{
public void Handle(Character character){
Console.WriteLine("캐릭터가 죽었습니다.");
}
}
// 컨텍스트 (캐릭터)
public class Character{
private ICharacterState _state;
public Character(ICharacterState initialState){
SetState(initialState);
}
public void SetState(ICharacterState newState){
_state = newState;
}
public void Act(){
_state.Handle(this);
}
}
// 사용 예
class Program{
static void Main(){
var character = new Character(new IdleState());
character.Act(); // Idle → Attack
character.Act(); // Attack → Dead
character.Act(); // Dead
}
}
이처럼 상태에 따른 행위가 Character 클래스에 직접 분기되지 않고, 각 상태 클래스에서 정의되므로 유지보수와 확장성이 매우 높다. 새로운 상태가 추가될 경우 기존 코드를 수정할 필요 없이 새 클래스만 만들면 된다. 하지만 단점은 상태 클래스 수가 많아질수록 코드가 분산되고 관리가 복잡해질 수 있으며, 단순한 조건 분기에는 오히려 과한 구조가 될 수 있다는 점이다. 또한 상태 전환 로직이 너무 복잡해지면 오히려 가독성이 떨어지기도 한다.
정리하자면 State 패턴은 동일 객체가 다양한 상태별로 다르게 행동해야 할 때 매우 적합하며, 게임 캐릭터나 AI의 상태 기계(Finite State Machine) 구현에 자주 활용된다. 상태별 책임 분리, 확장 가능성, 디버깅 편의성 측면에서 매우 강력한 설계 도구다.
Strategy(전략)
Strategy 패턴은 알고리즘(또는 행동)을 캡슐화하여 서로 교환 가능하도록 만드는 구조다. 즉, 같은 인터페이스를 가진 여러 전략(행동)을 정의해두고, 상황에 따라 객체에 전략을 주입함으로써 동작을 유연하게 바꿀 수 있게 한다.
게임에서는 몬스터 AI 행동 전환, 무기 공격 방식, 경로 탐색 방식, 이펙트 연출 로직 등에서 다양하게 사용된다.
// 전략 인터페이스
public interface IAttackStrategy{
void Attack();
}
// 전략 1: 근접 공격
public class MeleeAttack : IAttackStrategy{
public void Attack(){
Console.WriteLine("근접 공격 실행: 검으로 베기");
}
}
// 전략 2: 원거리 공격
public class RangedAttack : IAttackStrategy{
public void Attack(){
Console.WriteLine("원거리 공격 실행: 화살 발사");
}
}
// 전략 3: 마법 공격
public class MagicAttack : IAttackStrategy{
public void Attack(){
Console.WriteLine("마법 공격 실행: 파이어볼");
}
}
// 캐릭터(컨텍스트)
public class Character{
private IAttackStrategy _attackStrategy;
public void SetAttackStrategy(IAttackStrategy strategy){
_attackStrategy = strategy;
}
public void PerformAttack(){
if (_attackStrategy != null)
_attackStrategy.Attack();
else
Console.WriteLine("공격 전략이 설정되지 않음");
}
}
// 사용 예
class Program{
static void Main(){
var player = new Character();
player.SetAttackStrategy(new MeleeAttack());
player.PerformAttack(); // 근접 공격
player.SetAttackStrategy(new RangedAttack());
player.PerformAttack(); // 원거리 공격
player.SetAttackStrategy(new MagicAttack());
player.PerformAttack(); // 마법 공격
}
}
이처럼 Character는 구체적인 공격 방식에 의존하지 않고 IAttackStrategy 인터페이스만 알고 있기 때문에, 새로운 공격 방식이 생겨도 기존 코드를 수정하지 않고 새로운 전략 클래스를 추가하면 된다. 이로 인해 코드 유연성, 유지보수성, 확장성이 크게 향상된다.
단점은 단순한 경우에도 클래스 수가 증가하고, 전략 객체를 별도로 관리해야 한다는 점이다. 또한 전략 간 복잡한 의존 관계가 생기면 오히려 설계를 더 어렵게 만들 수 있다.
결론적으로 Strategy 패턴은 게임 캐릭터나 시스템이 다양한 행동을 선택적으로 수행해야 할 때, 특히 행동 변경이 동적으로 일어나야 할 때 이상적인 패턴이다. 행동을 캡슐화하여 깔끔하게 분리하고, 상황에 따라 전략을 갈아끼우는 구조를 통해 유지보수와 테스트를 훨씬 수월하게 만든다.
Template Method(템플릿 메서드)
Template Method 패턴은 상위 클래스에서 알고리즘의 골격을 정의하고, 세부 구현은 하위 클래스에 위임하는 구조다. 즉, 전체 실행 흐름은 고정하되, 그 안의 일부 단계는 하위 클래스가 자유롭게 구현하도록 만드는 패턴이다. 게임에서는 퀘스트 진행 흐름, 캐릭터 생성 절차, 특정 이벤트 처리 방식 등을 공통 구조로 유지하면서, 세부 구현만 각기 다르게 만들 때 자주 쓰인다.
// 추상 클래스: 캐릭터 생성 절차의 템플릿
public abstract class CharacterCreation{
// 템플릿 메서드: 캐릭터 생성 흐름 고정
public void CreateCharacter(){
SelectRace();
SelectClass();
CustomizeAppearance();
FinalizeCreation();
}
protected abstract void SelectRace();
protected abstract void SelectClass();
protected virtual void CustomizeAppearance(){
Console.WriteLine("기본 외형 설정 적용");
}
protected void FinalizeCreation(){
Console.WriteLine("캐릭터 생성 완료");
}
}
// 구체 클래스 1: 전사 캐릭터 생성
public class WarriorCreation : CharacterCreation{
protected override void SelectRace(){
Console.WriteLine("인간 종족 선택");
}
protected override void SelectClass(){
Console.WriteLine("전사 클래스 선택");
}
}
// 구체 클래스 2: 마법사 캐릭터 생성
public class MageCreation : CharacterCreation{
protected override void SelectRace(){
Console.WriteLine("엘프 종족 선택");
}
protected override void SelectClass(){
Console.WriteLine("마법사 클래스 선택");
}
protected override void CustomizeAppearance(){
Console.WriteLine("로브 색상 및 헤어스타일 설정");
}
}
// 사용 예
class Program{
static void Main(){
CharacterCreation warrior = new WarriorCreation();
warrior.CreateCharacter();
Console.WriteLine();
CharacterCreation mage = new MageCreation();
mage.CreateCharacter();
}
}
위 예시에서 CreateCharacter()는 상위 클래스에서 전체 절차를 정의하지만, SelectRace, SelectClass 등의 세부 단계는 하위 클래스가 구체화한다. 이로써 전체 구조는 일관되게 유지하면서도, 각 캐릭터 유형별로 유연하게 다르게 생성할 수 있다. 단점은 상속을 기반으로 하기 때문에 하위 클래스가 많아질수록 구조가 복잡해지고, 상위 클래스 변경 시 모든 하위 클래스에 영향이 갈 수 있어 신중한 설계가 필요하다는 점이다.
결론적으로 Template Method는 "공통 흐름은 고정하되, 세부 단계는 유연하게 바꾸고 싶을 때" 유용한 패턴이다. 게임 개발에서는 특히 퀘스트, 시나리오, 캐릭터 생성, 연출 흐름 등 일정한 절차가 있는 구조에 자주 활용된다.
Visitor(방문자)
Visitor 패턴은 객체 구조는 변경하지 않으면서, 그 구조를 순회하며 새로운 기능을 추가할 수 있도록 설계한 패턴이다. 핵심은 연산을 별도의 Visitor 객체로 분리해서, 데이터 구조와 연산을 각각 독립적으로 관리하는 데 있다. 게임에서는 맵 요소 순회, 캐릭터 효과 처리, 상태 검사, UI 요소 방문 처리 등에서 구조를 건드리지 않고 기능을 추가할 때 유용하다.
// 방문 대상 요소 인터페이스
public interface IGameElement{
void Accept(IGameVisitor visitor);
}
// 구체 요소 1: 보물 상자
public class TreasureChest : IGameElement{
public void Accept(IGameVisitor visitor){
visitor.Visit(this);
}
public void Open() => Console.WriteLine("보물상자가 열렸습니다!");
}
// 구체 요소 2: 몬스터
public class Monster : IGameElement{
public void Accept(IGameVisitor visitor){
visitor.Visit(this);
}
public void Inspect() => Console.WriteLine("몬스터 상태를 조사합니다.");
}
// 방문자 인터페이스
public interface IGameVisitor{
void Visit(TreasureChest chest);
void Visit(Monster monster);
}
// 방문자 1: 로그 기록 기능
public class LogVisitor : IGameVisitor{
public void Visit(TreasureChest chest){
Console.WriteLine("[Log] 보물상자 방문 기록");
chest.Open();
}
public void Visit(Monster monster){
Console.WriteLine("[Log] 몬스터 방문 기록");
monster.Inspect();
}
}
// 사용 예
class Program{
static void Main(){
List<IGameElement> elements = new List<IGameElement>{
new TreasureChest(),
new Monster(),
};
var logger = new LogVisitor();
foreach (var e in elements)
e.Accept(logger);
}
}
이처럼 Visitor 패턴을 사용하면 TreasureChest, Monster 같은 객체 구조는 건드리지 않으면서, LogVisitor, BuffVisitor, RenderVisitor처럼 다양한 기능을 외부에서 추가할 수 있다. 새로운 연산을 넣고 싶을 때는 Visitor 클래스를 추가하기만 하면 되므로 OCP(개방-폐쇄 원칙)에 잘 부합한다.
하지만 단점은 요소의 타입이 추가되면 모든 Visitor 클래스가 수정되어야 한다는 점이다. 즉, 요소를 확장하기는 어렵고, 연산을 확장하는 데 특화된 구조다. 또한 double dispatch 구조라서 코드 흐름이 다소 복잡하게 느껴질 수 있다.
결론적으로 Visitor는 많은 객체들이 일정한 구조를 갖고 있고, 그 위에 여러 기능을 올려야 할 때 강력한 설계 패턴이다. 특히 맵 객체, UI 노드, 시나리오 스텝 같은 구조가 자주 바뀌지 않고, 기능만 자주 추가되는 시스템에서 효과적이다.
참고
디자인 패턴 - 정리
서론 디자인 패턴은 협업하는 코드 구조를 설계할 때, 효율적이고 일관된 방식으로 문제를 해결하는 방법론이다. 특히 객체지향 프로그래밍에서 자주 마주치는 설계 문제들을 다룰 때, 디자인
jinger.tistory.com
디자인 패턴 - 생성 패턴
서론 디자인 패턴은 협업하는 코드 구조를 설계할 때, 효율적이고 일관된 방식으로 문제를 해결하는 방법론이다. 특히 객체지향 프로그래밍에서 자주 마주치는 설계 문제들을 다룰 때, 디자인
jinger.tistory.com
디자인 패턴 - 구조 패턴
서론 디자인 패턴은 협업하는 코드 구조를 설계할 때, 효율적이고 일관된 방식으로 문제를 해결하는 방법론이다. 특히 객체지향 프로그래밍에서 자주 마주치는 설계 문제들을 다룰 때, 디자인
jinger.tistory.com
'IT 진로 > 자격증' 카테고리의 다른 글
디자인 패턴 - 정리 (0) | 2025.06.30 |
---|---|
디자인 패턴 - 구조 패턴 (0) | 2025.06.30 |
디자인 패턴 - 생성 패턴 (0) | 2025.06.30 |
ISTQB_CTFL 핵심 정리 (0) | 2024.06.20 |
STQB_CTFL 6. 테스트 지원 도구 (1) | 2024.05.17 |
댓글