관련 영상
유니티 커맨드 패턴
커맨드 패턴
위키피디아-커맨드 패턴
설명 및 스도코드
- 요청(함수)을 객체(클래스)의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 매서드 이름, 매개변수 등 요청에 필요한 정보를 저장 또는 로깅, 취소할 수 있게 하는 패턴
- 요청 자체를 캡슐화함으로써 요청이 서로 다른 사용자를 매개변수로 만들고, 요청을 대기시키거나 로깅하며, 되돌릴 수 있는 연산 지원
- 함수 호출을 객체로 만들었기 때문에 디커플링으로 코드가 유용하고, 다른 클래스에 의존하지 않고 다양한 객체에 사용할 수 있다.
커맨드 패턴 구조
- 발송자(Invoker)
- 요청들을 시작하는 역할을 함
- 이 클래스에는 커맨드 객체에 대한 참조를 저장하기위 한 필드가 필요
- 발송자는 요청을 수신자에게 직접 보내는 대신 해당 커맨드를 작동시킴
- 발송자는 커맨드 객체를 생성할 책임이 없으며 일반적으로 생성자를 통해 클라이언트로부터 미리 생성된 커맨드를 받는다
- 커맨드 인터페이스(Command Interface)
- 일반적으로 커맨드를 실행하기 위한 단일 메서드만을 선언
- 구상 커맨드(Concrete Command)
- 다양한 유형의 요청 구현
- 자체적으로 작업을 수행하면 안되고, 대신 비즈니스 논리 객체 중 하나에 호출을 전달해야 함
- 수신 객체에서 메서드를 필요하는데 필요한 매개변수들을 구상 커맨드의 필드에 선언해 생성자로 초기화 가능. 이 때 생성자만 통하면서 커맨드 객체들을 불변으로 만들 수 있음
- 수신자(Receiver)
- 거의 모든 객체는 수신자 역할 가능.
- 대부분의 커맨드들은 요청이 수신자에게 전달되는 방법에 대한 세부 정보만 처리하는 반면 수신자 자체는 실제 작업 수행
- 클라이언트(Client)
- 구상 커맨드 객체들을 만들고 설정
- 수신자 인스턴스를 포함한 모든 요청 매개변수들을 커맨드의 생성자로 전달해야 함
- 그렇게 만들어진 커맨드는 하나 또는 여러 발송자와 연관될 수 있음
커맨드 패턴의 적용
- 작업들로 객체를 매개변수화하려는 경우 커맨드 패턴 사용
- 특정 메서드 호출을 독립실행형 객체로 전환해 커맨드들을 메서드 인수들로 전달하고, 이들을 다른 객체들의 내부에 저장하고, 런타임에 연결된 커맨드를 전환하는 등의 작업이 가능하다
- 상황에 맞는 메뉴 구현 혹은 게임 키 바인딩 구현 등에 사용 가능하다
- 작업들의 실행을 예약하거나, 작업들을 대기열에 넣거나, 작업들을 원격으로 실행하려는 경우
- 커맨드를 직렬화(파일이나 데이터베이스에 쉽게 쓸 수 있는 문자열로 변환하는 행위)해 커맨드를 외부에서 호출하기 편하게 할 수 있다.
- 온라인 게임에서 서버가 클라이언트에게 커맨드를 명령할 수 있다
- 되돌릴 수 있는 작업을 구현하려 할 때
- 실행/실행 취소/롤백 구현에 가장 많이 사용하는 패턴이다.
- 전략/보드게임의 롤백, TCG의 로그에도 유용하게 사용할 수 있다.
다른 패턴과의 관계
- 커맨드, 중재자, 옵서버, 책임 연쇄는 요청의 발신자와 수신자를 연결하는 다양한 방법을 다룬다.
- 책임 연쇄 패턴은 잠재적 수신자의 동적 체인을 따라 수신자 중 하나에 의해 요청이 처리될 때까지 요청을 순차적으로 전달
- 커맨드 패턴은 발신자와 수신자 간의 단방향 연결을 설립
- 중재자 패턴은 발신자와 수신자 간의 직접 연결을 제거하여 그들이 중재자 객체를 통해 간접적으로 통신하도록 강제
- 옵서버 패턴은 수신자들이 요청들의 수신을 동적으로 구독 및 구독 취소할 수 있도록 함
- 책임 연쇄 패턴의 핸들러들은 커맨드로 구현할 수 있다
- 그리할 시 다양한 작업을 같은 콘텍스트 객체에 대해 실행할 수 있으며, 해당 콘텍스트 객체는 요청(처리 메서드의 매개변수)의 역할을 함.
- 요청 자체가 커맨드 객체인 방식도 존재함. 이러할 시 같은 작업을 체인에 연결된 일련의 서로 다른 콘텍스트에서 실행 가능
- 실행 취소를 구현할 때 커맨드와 메멘토 패턴을 함께 사용 가능
- 커맨드들은 대상 객체에 대해 다양한 작업을 수행하는 역할 실행
- 메멘토들은 커맨드가 실행되기 직전에 해당 객체의 상태 저장
- 커맨드와 전략 패턴은 둘 다 작업을 매개변수화하는 데 사용할 수 있지만, 이 둘의 의도는 매우 다름
- 커맨드를 사용해 모든 작업을 객체로 변환할 수 있음. 작업의 매개변수들은 해당 객체의 필드들이 됨. 이 변환은 작업의 실행을 연기하고, 해당 작업을 대기열에 넣고, 커맨드들의 기록을 저장한 후 해당 커맨드들을 원격 서비스에 보내는 등의 작업을 가능하게 함
- 전략 패턴은 같은 작업을 수행하는 다양한 방법을 설명, 단일 콘텍스트 클래스 내에서 이러한 알고리즘들과 교환할 수 있도록 함
- 프로토타입은 커맨드 패턴의 복사본들을 기록에 저장해야 할 때 도움이 될 수 있음
- 비지터 패턴은 커맨드 패턴의 강력한 버전으로 취급할 수 있음. 비지터 패턴의 객체들은 다른 클래스들의 다양한 객체에 대한 작업 실행 가능
커맨드 패턴을 사용한 유니티 키 바인딩
- 해당 구현에서는 명령을 저장 후 방출할
Invoker
를 구현하지 않음
- 바인딩할 명령들에 대한 인터페이스(추상 커맨드)
// 인터페이스 : Execute() 메소드만 있는 추상클래스
public abstract class CommandKey {
public virtual void Execute(GameObject obj) {}
}
// Concrete Command 객체 : 직접적으로 동작하는 객체
public class CommandAttack : CommandKey {
// 객체를 파라미터로 받아 어떤 객체라도 메서드를 호출하여 사용할 수 있도록 함
public override void Execute(GameObject obj)
{
// 객체와 메서드는 decoupling 관계
Attack(obj);
}
void Attack(GameObject obj)
{
Debug.Log(obj.name + " Attack");
obj.transform.Translate (Vector3.forward);
}
}
// Concrete Command 객체 : 직접적으로 동작하는 객체
public class CommandDefense : CommandKey {
public override void Execute(GameObject obj)
{
Defense(obj);
}
void Defense(GameObject obj)
{
Debug.Log(obj.name + " Defense");
obj.transform.Translate (Vector3.back);
}
}
public class csPlayerCommand : MonoBehaviour {
CommandKey btnA, btnB;
void Start () {
SetCommand();
}
void SetCommand()
{
btnA = new CommandAttack();
btnB = new CommandDefense();
}
public void BtnCommandA()
{
btnA.Execute(gameObject); // 이 스크립트가 붙은 오브젝트를 공격하게 함
}
public void BtnCommandB()
{
btnB.Execute(gameObject); // 이 스크립트가 붙은 오브젝트를 방어하게 함
}
}
커맨드 패턴을 사용한 명령 롤백
public interface ICommand
{
void Execute();
void Undo();
}
public class UserInput : MonoBehaviour
{
public Lightbulb _lightbulb;
LightApp _lightApp;
void Start() {
// 인보커 세팅
_lightApp = new LightApp();
}
void Update() {
// 버튼을 누르면 인보커의 리스트에 커맨드를 추가하고 실행함
if (Input.GetKeyDown(KeyCode.Space)) {
ICommand togglePowerCommand = new TogglePowerCommand(_lightbulb);
_lightApp.AddCommand(togglePowerCommand);
}
else if (Input.GetKeyDown(KeyCode.C)) {
ICommand changeColorCommand = new ChangeColorCommand(_lightbulb);
_lightApp.AddCommand(changeColorCommand);
}
else if (Input.GetKeyDown(KeyCode.Z)) {
_lightApp.UndoCommand();
}
}
}
- 명령들을 저장하고 관리(실행/롤백 등)하는 클래스(인보커)
public class LightApp
{
// 명령 저장소
Stack<ICommand> _commandList;
public LightApp() {
_commandList = new Stack<ICommand>();
}
public void AddCommand(ICommand newCommand) {
newCommand.Execute();
_commandList.Push(newCommand);
}
public void UndoCommand() {
if (_commandList.Count > 0) {
ICommand latestCommand = _commandList.Pop();
latestCommand.Undo();
}
}
}
- 전구의 명령을 담고 있는 클래스(콘크리트 커맨드)
public class TogglePowerCommand : ICommand
{
// 전구 리시버 저장
Lightbulb _lightbulb;
// 전구를 세팅하는 생성자
public TogglePowerCommand(Lightbulb lightbulb) {
_lightbulb = lightbulb;
}
// 저장된 로직 실행
public void Execute() {
_lightbulb.TogglePower();
}
public void Undo() {
_lightbulb.TogglePower();
}
}
public class ChangeColorCommand : ICommand
{
// 전구 리시버 저장
Lightbulb _lightbulb;
// 이전 색 저장
Color _previousColor;
// 전구를 세팅하는 생성자
public ChangeColorCommand(Lightbulb lightbulb) {
_lightbulb = lightbulb;
_previousColor = lightbulb.GetComponent<Renderer>().material.color;
}
// 저장된 로직 실행
public void Execute() {
_lightbulb.SetRandomLightColor();
}
public void Undo() {
_lightbulb.SetLightColor(_previousColor);
}
}
public class Lightbulb : MonoBehaviour
{
bool isPowerOn = false;
public void TogglePower()
{
if (isPowerOn) {
GetComponent<Renderer>().material.DisableKeyword("_EMISSION");
transform.GetChild(0).gameObject.SetActive(false);
isPowerOn = false;
}
else {
GetComponent<Renderer>().material.EnableKeyword("_EMISSION");
transform.GetChild(0).gameObject.SetActive(true);
isPowerOn = true;
}
}
public void SetLightColor(Color newColor) {
Material material = GetComponent<Renderer>().material;
material.color = newColor;
material.SetColor("_EmissionColor", newColor);
transform.GetChild(0).gameObject.GetComponent<Light>().color = newColor;
}
public void SetRandomLightColor() {
Color randomColor = Random.ColorHSV(0f, 1f, 1f, 1f, 0,5f, 1f);
SetlightColor(randomColor);
}
}