콘텐츠로 이동

C++ 행동 패턴 — Command·Observer·State·Strategy·Template Method

행동 패턴(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가 이 개념을 구현한 예입니다.


알고리즘 군을 정의하고, 각각을 캡슐화하여 런타임에 교체 가능하게 만듭니다. AI 난이도, 병렬 알고리즘 교체, 플랫폼별 입력 처리에 활용합니다.

// AI 행동 전략 인터페이스
class IAIStrategy
{
public:
virtual ~IAIStrategy() = default;
virtual void Execute(class AIAgent& agent, float dt) = 0;
virtual std::string GetName() const = 0;
};
// 구체 전략: 순찰
class PatrolStrategy : public IAIStrategy
{
std::vector<glm::vec3> _waypoints;
int _currentWaypoint = 0;
public:
explicit PatrolStrategy(std::vector<glm::vec3> waypoints)
: _waypoints(std::move(waypoints)) {}
void Execute(AIAgent& agent, float dt) override;
std::string GetName() const override { return "Patrol"; }
};
// 구체 전략: 추격
class ChaseStrategy : public IAIStrategy
{
const glm::vec3* _targetPos;
public:
explicit ChaseStrategy(const glm::vec3* target) : _targetPos(target) {}
void Execute(AIAgent& agent, float dt) override;
std::string GetName() const override { return "Chase"; }
};
// 구체 전략: 도망
class FleeStrategy : public IAIStrategy
{
const glm::vec3* _threatPos;
public:
explicit FleeStrategy(const glm::vec3* threat) : _threatPos(threat) {}
void Execute(AIAgent& agent, float dt) override;
std::string GetName() const override { return "Flee"; }
};
// Context: 전략을 보유하고 실행
class AIAgent
{
std::unique_ptr<IAIStrategy> _strategy;
float _hp = 100.f;
public:
AIAgent() : _strategy(std::make_unique<PatrolStrategy>(
std::vector<glm::vec3>{{0,0,0},{10,0,0},{10,0,10}})) {}
void SetStrategy(std::unique_ptr<IAIStrategy> s)
{
std::cout << "AI: " << _strategy->GetName()
<< " -> " << s->GetName() << "\n";
_strategy = std::move(s);
}
void Update(float dt)
{
// HP에 따라 전략 자동 전환
if (_hp < 30.f && dynamic_cast<FleeStrategy*>(_strategy.get()) == nullptr)
SetStrategy(std::make_unique<FleeStrategy>(nullptr));
_strategy->Execute(*this, dt);
}
};

알고리즘의 골격을 기반 클래스에서 정의하고, 일부 단계를 서브클래스에서 구현합니다. “불변 부분은 기반 클래스, 변하는 부분은 서브클래스”가 핵심입니다.

// 기반 클래스: 게임 초기화 흐름의 골격 정의
class GameApplication
{
public:
// Template Method: 전체 흐름은 고정
void Run()
{
Initialize(); // 공통 초기화
LoadResources(); // 서브클래스가 구현
GameLoop(); // 공통 게임 루프
Shutdown(); // 공통 종료
}
protected:
// Hook Method: 서브클래스가 선택적으로 오버라이드
virtual void LoadResources() {} // 기본은 빈 구현
virtual void OnUpdate(float dt) = 0;
virtual void OnRender() = 0;
private:
void Initialize()
{
std::cout << "Window 생성, 렌더러 초기화\n";
}
void GameLoop()
{
while (_running)
{
float dt = CalculateDeltaTime();
OnUpdate(dt); // 서브클래스 구현
OnRender(); // 서브클래스 구현
SwapBuffers();
}
}
void Shutdown() { std::cout << "자원 해제\n"; }
bool _running = true;
float CalculateDeltaTime() { return 1.0f / 60.0f; }
void SwapBuffers() {}
};
// 구체 게임: 특정 단계만 구현
class MyGame : public GameApplication
{
protected:
void LoadResources() override
{
std::cout << "맵, 캐릭터 텍스처 로드\n";
}
void OnUpdate(float dt) override
{
// 게임 로직 업데이트
}
void OnRender() override
{
// 씬 렌더링
}
};

패턴핵심 목적게임 적용 사례
Command요청 캡슐화Undo/Redo, 리플레이, 입력 처리
Observer느슨한 이벤트 알림UI 갱신, 도전 과제, 퀘스트 추적
State상태별 행동 분리캐릭터 FSM, AI 상태, 게임 모드 전환
Strategy알고리즘 교체AI 난이도, 병렬 처리 전략
Template Method알고리즘 골격 정의게임 루프, 렌더링 패스

행동 패턴들은 독립적으로도 강력하지만 함께 사용하면 시너지가 극대화됩니다. 예를 들어 State 패턴으로 AI 상태를 관리하고, 상태 전환 시 Observer로 이벤트를 발행하며, 플레이어의 모든 행동을 Command로 기록하면 완성도 높은 게임 아키텍처를 구성할 수 있습니다. Strategy는 AI 전략을 런타임에 교체하고, Template Method는 게임 루프의 공통 흐름을 엔진 레이어에 고정시킵니다.