Tech Support — Core Client

디자인 패턴

Command & Observer — 게임 실무 적용

디자인 패턴에 대해 보통 부정적인 시각

  • 01 적용 시키면 구조가 더 복잡해진다.
  • 02 적용 시키면 퍼포먼스가 떨어진다.
  • 03 게임은 기획이 바뀌면 설계를 바꿔야 할 때가 있다.
    = 어차피 나중에 갈아 엎게 되어있다.

목차

  1. 1커맨드(Command) 패턴은 무엇인가?
  2. 2옵저버(Observer) 패턴은 무엇인가?
  3. 3게임 실무 사용 예
  4. 4Q&A

커맨드(Command) 패턴 BEHAVIORAL PATTERN · 01 / 06

📌 정의

요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 메서드 이름, 매개변수 등 요청에 필요한 정보를 저장·로깅·취소할 수 있게 하는 패턴.

이 패턴을 통해 실행(Do), 취소(Undo), 재실행(Redo), 명령 큐잉(Queuing), 로깅(Logging), 네트워크 전송(Remote Invocation) 등을 가능하게 하여 명령의 실행 시점과 호출 시점을 분리하고 유연한 명령 제어를 구현할 수 있다.

요청의 객체화 Do / Undo / Redo Command Queuing Logging Remote Invocation 실행·호출 시점 분리

커맨드(Command) 패턴 BEHAVIORAL PATTERN · 02 / 06

Command Pattern 흐름도 — action/request, ICommand interface, Command object, Command stack/buffer, Invoker

요청이 인터페이스를 거쳐 커맨드 객체로 포장되고, 히스토리나 버퍼에 쌓인 뒤 Invoker가 실행되는 흐름이다.

커맨드(Command) 패턴 BEHAVIORAL PATTERN · 03 / 06

Command
명령 인터페이스
Execute()와 선택적으로 Undo()를 선언하는 인터페이스. 모든 구체 커맨드가 이를 구현한다.
예: ICommand, ISkillCommand
ConcreteCommand
구체 명령
Command 인터페이스를 구현하며, Receiver에 실제 동작을 위임한다. 실행에 필요한 상태(방향, 거리 등)를 보유한다.
예: MoveCommand, AttackCommand
Receiver
수신자
실제 동작을 수행하는 대상. 커맨드로부터 호출받아 비즈니스 로직을 실행한다.
예: MoveCommandReceiver, Character, Inventory
Invoker
호출자
커맨드를 생성·스케줄·실행·취소하는 주체. 커맨드 히스토리를 보관해 Undo/Redo를 제공한다.
예: InputHandler, SkillManager, AIController

커맨드(Command) 패턴 BEHAVIORAL PATTERN · 04 / 06

Command Pattern UML — InputManager, CommandInvoker, ICommand, MoveCommand, PlayerMover

커맨드(Command) 패턴 BEHAVIORAL PATTERN · 05 / 06

Command Queue — Older to Newer, Undo, Current index, Redo

커맨드 큐에서 현재 인덱스를 기준으로 Undo와 Redo를 관리하는 구조다. 선형 히스토리 개념을 설명하기 쉽다.

커맨드(Command) 패턴 BEHAVIORAL PATTERN · 06 / 06

Undo stack and Redo stack with push and pop flow

자료구조 스택 두 개로 구현하는 가장 전형적인 형태다. 새 명령은 Undo 스택에 Push되고, Undo 시 Redo 스택으로 이동해 실행 취소와 재실행 흐름을 단순하게 유지할 수 있다.

커맨드(Command) 패턴 BEHAVIORAL PATTERN · 예제 코드

public interface ICommand
{
    void Execute();
    void Undo();
}

public class CommandInvoker
{
    // Undo용 커맨드 스택
    private static Stack<ICommand> s_UndoStack = new Stack<ICommand>();

    // Redo 가능한 커맨드를 저장하는 두 번째 스택
    private static Stack<ICommand> s_RedoStack = new Stack<ICommand>();

    // 커맨드를 즉시 실행하고 Undo 스택에 저장
    public static void ExecuteCommand(ICommand command)
    {
        command.Execute();
        s_UndoStack.Push(command);

        // 새로운 명령이 실행되면 Redo 이력은 무효화
        s_RedoStack.Clear();
    }

    public static void UndoCommand()
    {
        // 되돌릴 커맨드가 있으면
        if (s_UndoStack.Count > 0)
        {
            ICommand activeCommand = s_UndoStack.Pop();
            s_RedoStack.Push(activeCommand);
            activeCommand.Undo();
        }
    }

    public static void RedoCommand()
    {
        // 다시 실행할 커맨드가 있으면
        if (s_RedoStack.Count > 0)
        {
            ICommand activeCommand = s_RedoStack.Pop();
            s_UndoStack.Push(activeCommand);
            activeCommand.Execute();
        }
    }
}

ICommand는 Execute와 Undo를 공통 인터페이스로 제공하고, CommandInvoker는 Undo/Redo 스택을 분리해 실행 이력과 재실행 흐름을 함께 관리한다.

커맨드(Command) 패턴 BEHAVIORAL PATTERN · 정리

✅ 장점

  • 확장성 Invoker와 Receiver를 분리 → 새로운 커맨드·기능 추가 시 기존 코드 무수정
  • 시퀀싱 Undo/Redo, 매크로, 명령 큐 구현이 자연스럽게 따라옴
  • 입력 추상화 입력장치·플랫폼에 독립적인 행위 매핑 가능 (키보드·패드·AI 동일 처리)
  • 직렬화 커맨드 객체를 네트워크 전송·리플레이 파일 저장에 활용 가능

⚠️ 단점

  • 복잡성 각 명령이 독립 클래스 → 커맨드 수만큼 클래스 파일 증가
  • 이해도 패턴 미숙지 팀원에게 진입 장벽, 유지보수 시 전체 구조 파악 필요
  • 오버엔지니어링 단순 일회성 동작엔 과도한 설계 — 필요할 때만 도입

🎯 언제 쓰나

  • Undo / Redo / 리플레이가 필요할 때
  • 입력장치·플랫폼을 행위에서 분리하고 싶을 때
  • 매크로·스킬 시퀀싱·AI 행동 큐를 구현할 때
  • 명령을 네트워크로 직렬화·전송해야 할 때
  • 비동기 일괄 실행(작업 큐)이 필요할 때

옵저버(Observer) 패턴 BEHAVIORAL PATTERN · 01 / 05

📌 정의

하나의 객체(Subject)의 상태 변화가 있을 때, 그 객체와 연관된 다른 객체들(Observer)에게 자동으로 알림(Notify)이 전달되어 상태를 동기화하거나 반응하도록 하는 패턴.

주체와 구독자를 분리하여 느슨한 결합(Loose Coupling)을 달성하고, 이벤트 기반 시스템(Event-Driven Architecture) 구현을 용이하게 한다. UI 바인딩, 멀티플레이 동기화, MVVM 등 광범위하게 활용된다.

자동 알림(Notify) 느슨한 결합 이벤트 기반 Subscribe / Unsubscribe Publisher / Subscriber MVVM · UniRx · R3

옵저버(Observer) 패턴 BEHAVIORAL PATTERN · 02 / 05

Observer Pattern concept — Subject notifies three observers

Subject 하나가 여러 Observer에게 같은 시점에 알림을 전파하는 전형적인 브로드캐스트 구조다.

옵저버(Observer) 패턴 BEHAVIORAL PATTERN · 03 / 05

Subject
주체 / 발행자 / Publisher
상태를 보유하며 Observer 목록을 관리한다. Subscribe(), Unsubscribe()로 구독을 제어하고, 상태 변화 시 Notify()로 모든 Observer에게 알림을 전송한다.
예: GameStateManager, PlayerStats, EventBus
Observer
구독자 / Subscriber
Subject의 변화를 감시하며, 변경이 발생하면 Update() 콜백을 통해 반응한다. Subject에 대한 직접 참조 없이 느슨하게 결합된다.
예: HpBarUI, QuestTracker, SoundManager
Notification
이벤트 인자 / 메시지
상태 변경 시 Observer에게 전달되는 데이터. 무엇이 어떻게 바뀌었는지 담아 Observer가 적절히 반응하도록 한다.
예: PlayerDeadEvent, HpChangedArgs, StateChangedEvent

옵저버(Observer) 패턴 BEHAVIORAL PATTERN · 04 / 05

Observer Pattern UML — Subject, Observer1, Observer2 (C# 이벤트 방식)

옵저버(Observer) 패턴 BEHAVIORAL PATTERN · 예제 코드

public class Subject : MonoBehaviour
{
    // 델리게이트로 직접 이벤트 정의
    // public delegate void ExampleDelegate();
    // public static event ExampleDelegate ExampleEvent;

    // 또는 System.Action 사용
    public event Action ThingHappened;

    // 이벤트 발행 — 모든 구독자에게 전파
    public void DoThing()
    {
        ThingHappened?.Invoke();
    }
}

public class ExampleObserver : MonoBehaviour
{
    // 관찰 대상 Subject 참조
    [SerializeField] Subject subjectToObserve;

    // 이벤트 핸들러: Subject의 이벤트 시그니처와 일치해야 함
    private void OnThingHappened()
    {
        // 이벤트에 반응하는 로직 작성
    }

    private void Awake()
    {
        // Subject 이벤트 구독 등록
        if (subjectToObserve != null)
        {
            subjectToObserve.ThingHappened += OnThingHappened;
        }
    }

    private void OnDestroy()
    {
        // 오브젝트 소멸 시 구독 해제
        if (subjectToObserve != null)
        {
            subjectToObserve.ThingHappened -= OnThingHappened;
        }
    }
}

Subject는 ThingHappened 이벤트를 발행하고, ExampleObserver는 Awake에서 구독하고 OnDestroy에서 반드시 해제한다. 구독 해제를 빠뜨리면 메모리 누수가 발생한다.

옵저버(Observer) 패턴 BEHAVIORAL PATTERN · 05 / 05

✅ 장점

  • 느슨한 결합 Subject와 Observer가 인터페이스로만 연결 → 상호 수정 없이 독립 변경 가능
  • 재사용성 Subject와 Observer를 독립적으로 재사용하기 쉬움
  • 확장성 기존 코드 수정 없이 새 Observer만 추가하면 기능 확장 완료
  • 이벤트 기반 UI(HP바·코인·퀘스트), 네트워크, 데이터 변경 이벤트 처리에 적합

⚠️ 단점

  • 메모리 누수 Unsubscribe 누락 시 Observer가 GC되지 않음 — OnDestroy에서 반드시 해제
  • 순서 미보장 여러 Observer 간 호출 순서가 보장되지 않음
  • 순환 참조 Observer가 Subject를 다시 변경하면 무한 루프 위험
  • 디버깅 어려움 이벤트 체인이 길어질수록 흐름 추적이 복잡해짐

🎯 언제 쓰나

  • UI 바인딩 — HP, 코인, 퀘스트, 인벤토리 상태 표시
  • 게임 상태(Playing / Paused / GameOver) 전파
  • 이벤트 버스 — 시스템 간 직접 참조 없이 메시지 교환
  • 멀티플레이 상태 동기화 트리거
  • UniRx · R3 · MVVM 아키텍처 기반 설계