커맨드 패턴은 사용자가 보낸 요청 자체를 객체로 캡슐화하여 요청에 필요한 정보를 저장하거나 기록하여 나중에 재사용 가능하도록 하는 디자인패턴 중 하나다.
기존에는 객체가 할 수 있는 동작을 함수로 정의하고 호출하는 식으로 특정 동작을 시켜왔다.
커맨드 패턴을 활용하면 이런 동작을 객체로 표현하게 되어서 모듈화하여 저장하고 관리할 수 있는데 이는 Ctrl+Z나 Ctrl+Y 같은 Undo / Redo 기능을 만들어야하거나 사용자가 보낸 요청을 기록하여 순차적으로 실행하고, 다양한 동작을 동적으로 조합하여 실행하기에 매우 유용하다.
Command 인터페이스
모든 구체적인 명령어들이 동일한 방식으로 호출되도록 정의하는 인터페이스
public interface ICommand
{
void Execute(); // 실행
void Cancel(); // 실행 취소
void Undo(); // 실행 복구
}
만일 명령에 공통적으로 현재 동작이 가능한지 알려주는 bool CanExecute()가 필요하다면 여기에 먼저 추가를 해주어야 할 것이다.
ConcreteCommand
위의 Command 인터페이스를 구현하여 실제 동작을 표현하는 클래스
public class MoveCommand : ICommand
{
private Character character;
private Vector3 targetPos; // 목표 좌표
private Vector3 prePos; // 명령어를 시작하기 직전의 좌표
public MoveCommand(Character character, Vector3 targetPos)
{
prePos = character.transform.position;
this.character = character;
this.targetPos = targetPos;
}
public void Execute()
{
character.Move(targetPos); // 목표 위치로
}
public void Cancel()
{
character.MoveStop(); // 현재 위치에서 멈춤
}
public void Undo()
{
character.Move(prePos); // 원래 위치로
}
}
public class AttackCommand : ICommand
{
private Character character;
public AttackCommand(Character character)
{
this.character = character;
}
public void Execute()
{
character.Attack(); // 공격
}
public void Cancel()
{
character.CancelAttack(); // 공격 캔슬
}
public void Undo()
{
// 굳이 공격 동작을 되돌릴 이유가 있나?
}
}
Invoker
사용자의 요청을 Command 객체로 변환하여 저장하고 실행하는 역할
public class InputManager
{
// 실행되었던 명령어를 저장할 큐
private Stack<ICommand> cmdHistory = new Stack<ICommand>();
public void ExecuteCommand(ICommand command)
{
command.Execute();
cmdHistory.Push(command);
}
public void UndoCommand() // 실행취소
{
if (commandHistory.Count > 0)
{
var command = commandHistory.Pop();
command.Undo();
}
}
}
큐를 저장할 컬렉션은 List, Queue, Stack 등 다양하며, 물론 저장해두고 특정한 시점에 동시에 실행되게 할 수도 있다.
UndoCommand()를 통해 했던 동작을 역으로 되돌리는 것도 충분히 가능하다.
Receiver
명령 수행에 필요한 실제 작업을 구현한 객체
public class Character
{
public void Move(Vector3 pos)
{
Debug.Log($"{pos}로 이동합니다");
}
public void StopMove()
{
Debug.Log($"{transform.position}에서 멈춥니다");
}
public void Attack()
{
Debug.Log("공격!");
}
public void CancelAttack()
{
Debug.Log("공격 취소!");
}
}
모든 디자인 패턴이 그렇듯 커맨드 패턴도 적재적소에 사용하지 않으면 코드가 복잡해질 수 있다. 간단한 게임에서는 굳이 커맨드 패턴을 적용하기보다 기존처럼 함수 호출로 동작을 관리하는 편이 오히려 더 효율적이며, 커맨드 패턴은 특히 다양한 동작 기록 및 실행 취소, 재실행 요구가 있는 복잡한 시스템에 사용하는 것이 적절하다.