콘텐츠로 이동

C++ 행동 패턴 — Command·Observer·State

행동 패턴(Behavioral Pattern)은 객체 간의 상호작용과 책임 분배 방식을 정의합니다. 구조 패턴이 클래스 구성에 집중한다면, 행동 패턴은 런타임 동작 흐름을 어떻게 조율할지에 초점을 맞춥니다.

게임 개발에서 특히 중요한 세 가지 행동 패턴을 다룹니다.

  • Command: 요청을 객체로 캡슐화해 Undo/Redo, 리플레이 시스템 구현
  • Observer: 이벤트 기반 알림으로 UI 업데이트, 게임 이벤트 시스템 구현
  • State: 캐릭터 상태 머신을 명확하게 구조화

Command 패턴은 “무엇을 실행할 것인가”를 객체로 감쌉니다. 이를 통해 실행 요청을 큐에 쌓거나, 되돌리거나(Undo), 나중에 재실행(Redo)할 수 있습니다.

#include <memory>
#include <stack>
#include <vector>
#include <iostream>
// Command 인터페이스
class ICommand
{
public:
virtual ~ICommand() = default;
virtual void execute() = 0;
virtual void undo() = 0;
};
// 수신자(Receiver): 실제 작업을 수행하는 객체
class Character
{
int x = 0, y = 0;
public:
void move(int dx, int dy)
{
x += dx; y += dy;
std::cout << "Position: (" << x << ", " << y << ")\n";
}
// undo를 위해 역방향 이동 제공
void moveBack(int dx, int dy) { move(-dx, -dy); }
};
// 구체 커맨드: 이동
class MoveCommand : public ICommand
{
Character& character;
int dx, dy;
public:
MoveCommand(Character& ch, int dx, int dy)
: character(ch), dx(dx), dy(dy) {}
void execute() override { character.move(dx, dy); }
void undo() override { character.moveBack(dx, dy); }
};
// 인보커(Invoker): 커맨드를 관리하고 실행
class CommandManager
{
std::stack<std::unique_ptr<ICommand>> history;
public:
void executeCommand(std::unique_ptr<ICommand> cmd)
{
cmd->execute();
history.push(std::move(cmd));
}
void undoLast()
{
if (history.empty()) return;
history.top()->undo();
history.pop();
}
};
// 사용 예시
int main()
{
Character player;
CommandManager manager;
manager.executeCommand(std::make_unique<MoveCommand>(player, 1, 0)); // (1,0)
manager.executeCommand(std::make_unique<MoveCommand>(player, 0, 1)); // (1,1)
manager.undoLast(); // (1,0)으로 복귀
}
  • Undo/Redo 시스템: 에디터나 턴제 게임에서 행동 취소
  • 리플레이 시스템: 커맨드 큐를 저장했다가 재실행하면 동일한 게임 플레이 재현
  • 입력 리매핑: 키 입력을 커맨드 객체로 매핑하면 런타임에 조작 변경 가능
  • AI 행동 계획: AI가 계획한 행동을 커맨드 배열로 표현하고 순차 실행

Observer 패턴은 Subject(주제) 객체의 상태가 변경될 때 등록된 모든 Observer(관찰자)에게 자동으로 알림을 보냅니다. 발행-구독(Pub-Sub) 모델의 기초입니다.

#include <functional>
#include <unordered_map>
#include <vector>
#include <algorithm>
// 이벤트 타입을 enum으로 정의
enum class GameEvent
{
PlayerDied,
LevelUp,
ItemPickup,
EnemyDefeated
};
// 이벤트 데이터 (다형성 활용 가능)
struct EventData
{
int intValue = 0;
float floatValue = 0.f;
// 필요에 따라 확장
};
// EventDispatcher: Subject 역할
class EventDispatcher
{
using Handler = std::function<void(const EventData&)>;
std::unordered_map<GameEvent, std::vector<Handler>> listeners;
public:
// 리스너 등록 (람다, 멤버함수 포인터 모두 가능)
void subscribe(GameEvent event, Handler handler)
{
listeners[event].push_back(std::move(handler));
}
// 이벤트 발행
void dispatch(GameEvent event, const EventData& data = {}) const
{
auto it = listeners.find(event);
if (it == listeners.end()) return;
for (const auto& handler : it->second)
{
handler(data);
}
}
};
// 전역 또는 싱글턴으로 사용
EventDispatcher gEventDispatcher;
// Observer 사용 예시
class HUD
{
public:
HUD()
{
// 람다로 이벤트 구독
gEventDispatcher.subscribe(GameEvent::LevelUp,
[this](const EventData& data)
{
onLevelUp(data.intValue);
});
gEventDispatcher.subscribe(GameEvent::PlayerDied,
[this](const EventData&)
{
showGameOver();
});
}
private:
void onLevelUp(int newLevel)
{
// UI 업데이트 로직
}
void showGameOver() { /* ... */ }
};
// Subject(발행자) 사용 예시
class Player
{
int level = 1;
public:
void gainExperience(int exp)
{
// 레벨업 조건 충족 시 이벤트 발행
EventData data;
data.intValue = ++level;
gEventDispatcher.dispatch(GameEvent::LevelUp, data);
}
};

Observer가 먼저 소멸하면 등록된 핸들러가 해제된 메모리를 참조할 수 있습니다. 해결책으로는 다음이 있습니다.

  • 구독 취소(unsubscribe) 메서드로 소멸자에서 해제
  • std::weak_ptr로 Observer 참조를 약하게 유지
  • 핸들러 ID를 반환해 명시적 구독 해제

State 패턴은 객체의 내부 상태에 따라 행동이 달라질 때, 각 상태를 별도 클래스로 분리합니다. 복잡한 if-else 또는 switch 분기를 제거하고 상태 전환 로직을 명확하게 합니다.

#include <memory>
#include <iostream>
class Character; // 전방 선언
// State 인터페이스
class ICharacterState
{
public:
virtual ~ICharacterState() = default;
virtual void update(Character& character, float deltaTime) = 0;
virtual void onEnter(Character& character) {}
virtual void onExit(Character& character) {}
};
// 컨텍스트: 상태를 보유하는 캐릭터
class Character
{
std::unique_ptr<ICharacterState> currentState;
float speed = 0.f;
public:
float moveInput = 0.f;
bool isGrounded = true;
bool jumpPressed = false;
explicit Character(std::unique_ptr<ICharacterState> initialState)
{
transitionTo(std::move(initialState));
}
void transitionTo(std::unique_ptr<ICharacterState> newState)
{
if (currentState) currentState->onExit(*this);
currentState = std::move(newState);
currentState->onEnter(*this);
}
void update(float deltaTime)
{
currentState->update(*this, deltaTime);
}
void setSpeed(float s) { speed = s; }
float getSpeed() const { return speed; }
};
// 구체 상태: 유휴(Idle)
class IdleState : public ICharacterState
{
public:
void onEnter(Character& ch) override
{
ch.setSpeed(0.f);
std::cout << "[State] Idle\n";
}
void update(Character& ch, float dt) override;
// (아래에서 정의)
};
// 구체 상태: 이동(Run)
class RunState : public ICharacterState
{
public:
void onEnter(Character& ch) override
{
ch.setSpeed(300.f);
std::cout << "[State] Run\n";
}
void update(Character& ch, float dt) override;
};
// 구체 상태: 점프(Jump)
class JumpState : public ICharacterState
{
float timeInAir = 0.f;
public:
void onEnter(Character& ch) override
{
timeInAir = 0.f;
std::cout << "[State] Jump\n";
}
void update(Character& ch, float dt) override
{
timeInAir += dt;
if (timeInAir > 0.8f && ch.isGrounded)
{
// 착지 후 입력에 따라 상태 전환
if (ch.moveInput != 0.f)
ch.transitionTo(std::make_unique<RunState>());
else
ch.transitionTo(std::make_unique<IdleState>());
}
}
};
// update 정의 (순환 의존 해결 후)
void IdleState::update(Character& ch, float dt)
{
if (ch.jumpPressed)
ch.transitionTo(std::make_unique<JumpState>());
else if (ch.moveInput != 0.f)
ch.transitionTo(std::make_unique<RunState>());
}
void RunState::update(Character& ch, float dt)
{
if (ch.jumpPressed)
ch.transitionTo(std::make_unique<JumpState>());
else if (ch.moveInput == 0.f)
ch.transitionTo(std::make_unique<IdleState>());
}

기본 State 패턴을 확장해 상태를 계층으로 구성하면 “공중에 있는 동안 공격 가능” 같은 복합 상태를 간결하게 표현할 수 있습니다. Unreal Engine의 Animation Blueprint State Machine과 Unity의 Animator Controller가 이 개념을 구현한 예입니다.


패턴핵심 목적게임 적용 사례
Command요청 캡슐화Undo/Redo, 리플레이, 입력 처리
Observer느슨한 이벤트 알림UI 갱신, 도전 과제, 퀘스트 추적
State상태별 행동 분리캐릭터 FSM, AI 상태, 게임 모드 전환

Command, Observer, State 패턴은 독립적으로도 강력하지만 함께 사용하면 시너지가 극대화됩니다. 예를 들어 State 패턴으로 AI 상태를 관리하고, 상태 전환 시 Observer로 이벤트를 발행하며, 플레이어의 모든 행동을 Command로 기록하면 완성도 높은 게임 아키텍처를 구성할 수 있습니다.

다음 단계로는 Strategy 패턴(알고리즘 교체), Chain of Responsibility 패턴(입력 처리 체인), Visitor 패턴(다형적 연산 분리)을 학습하는 것을 권장합니다.