커맨드 패턴

정선호·2023년 5월 7일
0

Design Patterns

목록 보기
6/24

관련 영상
유니티 커맨드 패턴

커맨드 패턴

위키피디아-커맨드 패턴
설명 및 스도코드

  • 요청(함수)을 객체(클래스)의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 매서드 이름, 매개변수 등 요청에 필요한 정보를 저장 또는 로깅, 취소할 수 있게 하는 패턴
    • 요청 자체를 캡슐화함으로써 요청이 서로 다른 사용자를 매개변수로 만들고, 요청을 대기시키거나 로깅하며, 되돌릴 수 있는 연산 지원
    • 함수 호출을 객체로 만들었기 때문에 디커플링으로 코드가 유용하고, 다른 클래스에 의존하지 않고 다양한 객체에 사용할 수 있다.

커맨드 패턴 구조

  • 발송자(Invoker)
    • 요청들을 시작하는 역할을 함
    • 이 클래스에는 커맨드 객체에 대한 참조를 저장하기위 한 필드가 필요
    • 발송자는 요청을 수신자에게 직접 보내는 대신 해당 커맨드를 작동시킴
    • 발송자는 커맨드 객체를 생성할 책임이 없으며 일반적으로 생성자를 통해 클라이언트로부터 미리 생성된 커맨드를 받는다
  • 커맨드 인터페이스(Command Interface)
    • 일반적으로 커맨드를 실행하기 위한 단일 메서드만을 선언
  • 구상 커맨드(Concrete Command)
    • 다양한 유형의 요청 구현
    • 자체적으로 작업을 수행하면 안되고, 대신 비즈니스 논리 객체 중 하나에 호출을 전달해야 함
    • 수신 객체에서 메서드를 필요하는데 필요한 매개변수들을 구상 커맨드의 필드에 선언해 생성자로 초기화 가능. 이 때 생성자만 통하면서 커맨드 객체들을 불변으로 만들 수 있음
  • 수신자(Receiver)
    • 거의 모든 객체는 수신자 역할 가능.
    • 대부분의 커맨드들은 요청이 수신자에게 전달되는 방법에 대한 세부 정보만 처리하는 반면 수신자 자체는 실제 작업 수행
  • 클라이언트(Client)
    • 구상 커맨드 객체들을 만들고 설정
    • 수신자 인스턴스를 포함한 모든 요청 매개변수들을 커맨드의 생성자로 전달해야 함
    • 그렇게 만들어진 커맨드는 하나 또는 여러 발송자와 연관될 수 있음

커맨드 패턴의 적용

  1. 작업들로 객체를 매개변수화하려는 경우 커맨드 패턴 사용
    • 특정 메서드 호출을 독립실행형 객체로 전환해 커맨드들을 메서드 인수들로 전달하고, 이들을 다른 객체들의 내부에 저장하고, 런타임에 연결된 커맨드를 전환하는 등의 작업이 가능하다
    • 상황에 맞는 메뉴 구현 혹은 게임 키 바인딩 구현 등에 사용 가능하다
  2. 작업들의 실행을 예약하거나, 작업들을 대기열에 넣거나, 작업들을 원격으로 실행하려는 경우
    • 커맨드를 직렬화(파일이나 데이터베이스에 쉽게 쓸 수 있는 문자열로 변환하는 행위)해 커맨드를 외부에서 호출하기 편하게 할 수 있다.
    • 온라인 게임에서 서버가 클라이언트에게 커맨드를 명령할 수 있다
  3. 되돌릴 수 있는 작업을 구현하려 할 때
    • 실행/실행 취소/롤백 구현에 가장 많이 사용하는 패턴이다.
    • 전략/보드게임의 롤백, 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);
    }
}
profile
학습한 내용을 빠르게 다시 찾기 위한 저장소

0개의 댓글