오늘은 Command 패턴에 대해 알아보는 시간을 가지겠습니다.

- 요청을 객체로 캡슐화하여, 행동 실행의 호출자와 수행자를 분리하고 행동을 기록하거나 취소할 수 있는 디자인 패턴입니다.
- 즉, 이벤트가 발생했을 때 실행될 기능이 다양하면서도 변경이 필요한 경우에 이벤트를 발생시키는 클래스를 변경하지 않고 재사용하고자 할 때 유용합니다.
- 행동 캡슐화
- 명령을 하나의 객체로 표현하여 동적으로 행동을 변경하거나 저장이 가능합니다.
- 행동 기록 및 취소
- 명령을 히스토리로 관리하여 취소 기능을 구현할 수 있습니다.
- 행동 실행 분리
- 명령을 호출하는 객체와 실행하는 객체 간의 결합도를 낮춥니다.
public interface ICommand
{
void Execute();
void Undo();
}
- 행동을 추상화한 인터페이스 또는 추상 클래스입니다.
- 일반적으로 Execute()와 Undo() 메서드를 포함합니다.
public class MoveCommand : ICommand
{
private Transform _character;
private Vector3 _previousPosition;
private Vector3 _newPosition;
public MoveCommand(Transform character, Vector3 newPosition)
{
_character = character;
_newPosition = newPosition;
}
public void Execute()
{
_previousPosition = _character.position; // 이전 위치 저장
_character.position = _newPosition; // 새로운 위치로 이동
}
public void Undo()
{
_character.position = _previousPosition; // 이전 위치로 되돌림
}
}
- Command 인터페이스를 구현하며, 특정 행동을 정의합니다.
- 예를 들어, MoveCommand, AttackCommand 등이 있을 수 있습니다.
public class InputHandler : MonoBehaviour
{
private Stack<ICommand> _commandHistory = new Stack<ICommand>();
public Transform character;
void Update()
{
if (Input.GetKeyDown(KeyCode.W))
{
ExecuteCommand(new MoveCommand(character, character.position + Vector3.up));
}
if (Input.GetKeyDown(KeyCode.S))
{
ExecuteCommand(new MoveCommand(character, character.position + Vector3.down));
}
if (Input.GetKeyDown(KeyCode.Z)) // Undo 명령
{
UndoCommand();
}
}
void ExecuteCommand(ICommand command)
{
command.Execute();
_commandHistory.Push(command); // 명령 저장
}
void UndoCommand()
{
if (_commandHistory.Count > 0)
{
ICommand lastCommand = _commandHistory.Pop();
lastCommand.Undo();
}
}
}
- 명령을 실행하거나 취소하는 역할을 담당합니다.
- 예를 들어, 플레이어의 입력을 처리하는 역할
public class Character : MonoBehaviour
{
public void Move(Vector3 position)
{
transform.position = position;
}
}
- 실제로 명령을 수행하는 객체입니다.
- 캐릭터, 몬스터, 또는 UI 요소 등이 될 수 있습니다.
- 사용자 입력
- 명령 실행
- 명령 기록
- 실행 취소
위 코드를 예시로 들자면
- Input.GetKeyDown(KeyCode.W) : 사용자 입력
- Execute() : 명령 실행
- _commandHistory.Push(command) : 명령 기록
- UndoCommand() : 실행 취소
- 장점
- 명령을 모듈화하여 재사용성 증가
- 행동 기록 및 되돌리기 기능 제공
- 호출자와 수신자 간 결합도를 낮춤
- 단점
- 간단한 로직에도 클래스가 늘어날 수 있음
- 히스토리 관리 시 메모리 사용량이 증가할 수 있음

흐름도에서 예측한 것 처럼 흘러갑니다.
하지만 InputKey를 받을 때마다 New Command가 생성되어 성능 문제가 발생할 것 같습니다...
public class CommandPool<T> where T : ICommand, new()
{
private Stack<T> _pool = new Stack<T>();
public T Get()
{
if (_pool.Count > 0)
{
return _pool.Pop();
}
return new T();
}
public void Release(T command)
{
_pool.Push(command);
}
}
- T 형식으로 ICommand를 구현한 모든 클래스 타입을 받을 수 있도록 구현하였으며 뒤에 new()를 사용하여 매개 변수 없는 기본 생성자를 가져야 하도록 하였습니다.
- Get(),Release()는 간단하게 풀을 가져오거나 반환하도록 하였으며 Stack이 비어있으면 새로운 객체를 생성하도록 하였습니다.
public class MoveCommand : ICommand
{
private Transform _character;
private Vector3 _previousPosition;
private Vector3 _newPosition;
public void Initialize(Transform character, Vector3 newPosition)
{
_character = character;
_newPosition = newPosition;
}
public void Execute()
{
_previousPosition = _character.position;
_character.position = _newPosition;
}
public void Undo()
{
_character.position = _previousPosition;
}
}
- 기존 생성자 함수는 주석처리 해주어야 합니다.
- 기존 코드에서 Initialize(Transform,Vector3)을 추가해줍니다.
public class InputHandler : MonoBehaviour
{
private Stack<ICommand> _commandHistory = new Stack<ICommand>();
private CommandPool<MoveCommand> _commandPool = new CommandPool<MoveCommand>();
public Transform character;
void Update()
{
if (Input.GetKeyDown(KeyCode.W))
{
Move(Vector3.up);
}
if (Input.GetKeyDown(KeyCode.S))
{
Move(Vector3.down);
}
if (Input.GetKeyDown(KeyCode.Z)) // Undo 명령
{
UndoCommand();
}
}
void Move(Vector3 direction)
{
MoveCommand command = _commandPool.Get();
command.Initialize(character, character.position + direction);
command.Execute();
_commandHistory.Push(command);
}
void UndoCommand()
{
if (_commandHistory.Count > 0)
{
ICommand lastCommand = _commandHistory.Pop();
lastCommand.Undo();
_commandPool.Release((MoveCommand)lastCommand);
}
}
}
- New Command() 사용 부분이 없어지고 Initialize()로 대체되었으며
- ExecuteCommand(ICommand) -> Move(Vector3)로 대체되었습니다.
- 그 외에도 여러 방법이 있습니다..
- ex) Command 재사용, 타이머를 걸어서 명령 실행 빈도를 조절 및 필터링을 걸어 이전 명령과 동일한 명령 기록하지 않도록 처리..
Ctrl + Z 키를 누른 것처럼 이전에 실행했던 행동으로 돌아가는게 신기하면서도 즐거웠습니다.
새로운 명령을 추가할 때에도 큰 어려움이 없을 것 같고 AI 행동 패턴에 적용하면 재밌을 것 같습니다. 이전 시간에 배운 Observer 패턴과 같이 조합하면 AI 설계에 굉장히 도움이 될 것 같습니다!