본문 바로가기
개발 일지/디자인패턴 & 아키텍쳐

명령패턴

by osul_world 2021. 12. 29.
728x90

명령패턴


사용자 요청에 따라 수행되는 로직들을 명령패턴을 이용하면 각 로직들을 객체로 관리하고 이를 조합하여

자동 매크로를 만들수도있다. 또한 그 로직들을 여러개의 사용자 객체가 공유하여 사용할수도있다.

확장성 느슨한 의존관계 유연성

 

손님은 요리사에게 직접 주문하지 않는다. 손님은 알바생에게 주문하면 알바생이 알맞는 주문을 처리하여 요리사에게 전달한다. 손님(sender) <느슨한 의존관계> 요리사(receiver)

요리의 생성과정을 몰라도 된다.

주문방식에 대해서 세밀하게 알 필요가 없다.

주문(명령)처리는 알바생이 손님의 간단한 요청을 가공하여 요리사에게 전달 처리한다.

  • 메소드 요청을 객체화 하고 캡슐화하여 다양한 요청 처리(파라미터 이용 등)를 할수있도록 하는 패턴.

    클라이언트에게 전적으로 위임되던 요청정보를 코드화 할수있음

    요청 로그 정보를 히스토리화 하기 좋음(큐,파일시스템에 저장)

    요청의 취소기능을 제공할수있다

    요청객체와 요청수행 객체 사이의 의존관계를 느슨하게 유지할수있음

  • 기존명령vs명령패턴

기존 명령 방법
Sender -> Receiver //직접 Receiver 객체의 함수를 호출 및 파라미터를 이용하여 연산수행

명령패턴
Sender -> CommandManager -> Receiver //CommandManager를 활용하여 명령을 객체화 연산 대행

CommandManager는 다양한 Command 객체를 관리한다.

  • 패턴 구성

command interface: 구체적 명령 객체를 위한 interface, 보통 execute와 undo 메소드를 포함하고 있음

concrete command: 처리자와 행동을 연결함. 처리자가 제공하는 메소드를 이용하여 실제 행동을 구현함.

concrete command은 receiver와 의존관계를 맺음

실행자(invoker): 요청의 실행을 요구함.

보통 명령 객체들을 유지함

처리자(receiver): 요청을 수행하기 위해 해야 하는 기능이 구현되어 있는 객체. 어떤 종류의 객체도 처리자가 될 수 있음


클라이언트(TestCode): 구체적 명령 객체를 생성하고 처리자와 연결함. 실행자에게 명령의 실행을 요청함. 명령 객체를 생성하는 클라이언트와 명령의 실행을 요청하는 클라이언트는 서로 다를 수 있음

reciever & concreteCommand 와 의존관계를 맺고있다.

image

  • 실제코드
//command interface
//concrete command들을 위한 공통리모컨 및 설계도 역할
public interface Command {
	void execute();
 void undo();
//void redo();
/**
undo method는 실행된 excute를 취하해야한다.

redo method는 가장 최근 실횡된 취소명령을 복구한다. 단, 실행명령이 들어오면 redo는 초기화
*/
}
//receiver 
//명령수행을 위한 정의가 포함된 객체: like 요리사
public class RoomLight {
	public void on(){
		System.out.println("");
	}
	public void off(){
		System.out.println("");
	}
}
//concrete command
//특정 명령을 수행하는 명령객체 implements Command & has-a receiver
//세부적인 역할에 따라 세부 구분하기

//concrete command1
public class RoomLightOnCommand implements Command {
	private RoomLight light; //receiver has-a

public RoomLightOnCommand(RoomLight light){ //dip
      this.light = light;
  }
//override
public void execute() {
      light.on();
}
//override
public void undo(){
light.off();
}

} 

//concrete command2
public class RoomLightOffCommand implements Command {
private RoomLight light; //receiver has-a

public RoomLightOffCommand(RoomLight light){ //dip
      this.light = light;
  }
//override
public void execute() {
      light.off();
  }
//override
public void undo(){ 
light.on();
}
} 
//굳이 RoomLightOnCommand의 반대 행동을 하는 RoomLightOffCommand 도 만들어야 하나?
/**
만들어야한다. undo란 가장최근에 실행된 명령을 취소하는 커멘드 이기때문에 undo 메소드 내용도 반대로     적용되야한다.
*/

//명령어가 인자를 필요로 하는경우
//invoker
public class RemoteControl {
private Command[] onCommands = new Command[7];
private Command[] offCommands = new Command[7];
private Command undoCommand = new NoCommand();

//모든 리모컨 슬롯 초기화 NoCommand라는 빈 객체를 이용, null exception 방지
public RemoteControl(){
    Command noCommand = new NoCommand();//빈 객체
    Arrays.fill(onCommands, noCommand);
    Arrays.fill(offCommands, noCommand);
    undoCommand = noCommand;
}
/** NoCommand class: 빈 커멘드 객체
public class NoCommand implements Command {
      public void execute() {}
      public void undo() {}
  }
  //빈 커멘드 객체를 사용하면 Pressed 메서드들에서 예외처리 없어도 됨
*/

public void setCommand(int slot, Command onCommand, Command offCommand)    { … }

//on 버튼
public void onButtonWasPressed(int slot){
/**
if(onCommands[slot]!=null) //null 대신 빈 커맨드 객체를 활용, 예외처리 필요 x
*/
onCommands[slot].execute(); //slot: 몇번째 버튼을 눌렀는지
undoCommand = onCommand[slot]; //undoCommand에 최근 execute() 객체 save
}

	//off 버튼
public void offButtonWasPressed(int slot){
offCommands[slot].execute();
undoCommand = onCommand[slot]; 
}

public void undoButtonPressed(){
  undoCommand.undo();
}
} // Invoker
//모든 명령어를 컨트롤하는 실행자 like 서빙직원
//client testCode
class Client{
psvm(){
SimpleRemoteControl sr = new SimpleRemoteControl;
sr.setCommand(new RoomLightOnCommand(new Roomlight()));

sr.onButtonWasPressed(1) //client는 버튼을 누루기만하면된다 커멘드실행은 invoker에게 위임 -> receiver와의 느슨한 의존관계 
}  
}

위 예제의 undo는 가장 최근의 excuate된 객체로 갱신된다. undo나 redo를 stack이나 자료구조를 이용하여 목록화 해두어 직전실행만이 아닌 이전에 실행도 자유롭게 undo/redo 할수있도록 해보자.

 

스택 데이터로 undo목록과 redo목록을 유지

//invoker
public class CommandManager {
    private Stack<Command> undoStack = new Stack<>(); //스텍 데이터 관리
    private Stack<Command> redoStack = new Stack<>();
    
    public void execute(Command… commands){
        for(var command: commands){
            undoStack.push(command);
            command.execute();
        }
    	redoStack.clear();
    }
    //undo는 가장 최근에 실행된 명령을 취소해야하고
    public void undo() {
        if(!undoStack.isEmpty()) {
            Command command = undoStack.pop();
            redoStack.push(command); //redo 스택에 추가
            command.undo(); //undo
        }
    }
    //redo는 가장 최근 실횡된 취소명령을 복구한다. 단, 실행명령이 들어오면 redo는 초기화
    public void redo() {
        if(!redoStack.isEmpty()) {
            Command command = redoStack.pop();
            undoStack.push(command); //undo 스택에 추가
            command.execute(); //excute
        }
    }
}

후입 선출의 stack으로 목록을 유지하지 않아도 된다. 가장 최근 명령이 아니라 자유롭게 stack안 목록을 undo하면?

아래 문제 발생가능

 

undo의 문제점

스택에 담기는 명령 객체는 같은 메모리 주소를 참조한다.

ex) 1 3 1 버튼 입력시 undo 스택에 담긴 1 3 1 의 첫번째 1과 3번째 1은 같은 객체이다.

명령 객체(concrete command)가 이전 상태를 저장하는 커맨드의 경우라고 해보자

선풍기를 예제로 들겠다.

excute 1 : 초기 상태 off 에서 강도 1로 선풍기를 켜고 상태를 1로 변경

excute 3 : 현재 상태 1에서 강도를 3으로 바꾸고 상태를 3으로 변경

excute 1: 현재상태 3에서 강도를 1로 바꾸고 상태를 1으로 변경

여기서 첫번째 excute 1 과 3번째 excute1 은 같은 주소를 바라본다고 했다.

첫 1 명령객체는 이전상태를 off로 저장하고있었다.

세번쨰 1 명령객체는 이전 상태를 3으로 저장하고있다.

이때 첫 1 명령객체의 이전상태 off가 3으로 덮어씌워져 버린다.

만약 자유롭게 undo를 하는 사용자가 첫 1명령객체를 undo한다면 선풍기는 초기상태 off로 돌려보내야하는데

아까 덮어씌워진 3으로 돌려보내지는 문제가 발생한다.

따라서 매번 새롭게 new 하여 명령객체를 만들어서 처리해야한다.

https://www.youtube.com/watch?v=Yy-bMuHhhBc&t=8s [4:55~] 참고

 

[응용]

매크로 커맨드

명령객체들을 리스트화 하여 일종의 커멘드화 시킬수있다.

가변배열+forEach로 유연하게 처리하면 리스트가 변경되어도 코드를 수정할 필요가없다.

public class MacroCommand implements Command{
    private Command[] commands;
    
    public MacroCommand(Commands[] commands){
    	// public MacroCommand(Commands… commands){
    	commands = coms;
    }
    
    public void execute(){
        for(Command com: commands)
        com.execute();
        // Arrays.stream(commands).forEach(Command::execute);
    }
}

 

receiver가 상태인자가 필요한 경우

3가지 방법이 있지만 모두 각각의 단점을 가지고있다.

방법1

public class CeilingFanCommand implements Command{    private CeilingFan fan;    private int prevSpeed;    private int fanSpeed;        // 방법 1: concrete command 생성자 호출시 파라미터를 입력받기    // 다양한 인자를 이용하여 명령 객체를 실행해야 하면 인자 값마다 다른 명령 객체의 생성이 필요할 수 있음.    public CeilingFanCommand(    CeilingFan fan, int fanSpeed){        this.fan = fan;        this.fanSpeed = fanSpeed;    }         public void execute(){    	prevSpeed = ceilingFan.getSpeed();    	fan.setSpeed(fanSpeed);    }        public void undo(){    	fan.setSpeed(prevSpeed);    }}

방법2

public class CeilingFanCommand implements Command{    private CeilingFan fan;    private int prevSpeed;    private int fanSpeed;        public CeilingFanCommand(CeilingFan fan){    	this.fan = fan;    }        // 방법 2:생성자가 x setter를 이용하여 파라미터 받기    // 클라이언트가 실행을 요청하기 전에 인자를 별도로 전달해야 하는 번거로움이 있고, 모든 명령 객체가 균일한 방법으로 인자를 설정하는 방법을 제공하기 힘들 수 있음.    public void setSpeed(int fanSpeed){     	this.fanSpeed = fanSpeed;    }         public void execute(){   	 	prevSpeed = ceilingFan.getSpeed();    	fan.setSpeed(fanSpeed);    }        public void undo(){    	fan.setSpeed(prevSpeed);    }}

방법3

public class CeilingFanCommand implements Command{    private CeilingFan fan;    private int prevSpeed;    private int fanSpeed;        public CeilingFanCommand(CeilingFan fan){    	this.fan = fan;    }    	// 방법3. execute가 매개변수를 가짐    // 다양한 명령을 추상화하는 것이 번거롭게 됨    public void execute(int fanSpeed){     	prevSpeed = ceilingFan.getSpeed();    	fan.setSpeed(fanSpeed);    }        public void undo(){    	fan.setSpeed(prevSpeed);    }}

 

정리

추가사항

명령을 큐에 저장해두고 쓰래드를 이용해 동시에 여러 명령어를 실행하도록 할수있다.

파일시스템에 명령어적용 상태를 저장해두었다가 그대로 load할수있다.

 

관련 리펙토링

조건문에따라 처리가 달라지는 부분이 많은 코드는 명령패턴을 이용하는것이 옳다

 

패턴의 특성: Behavior

패턴의 수준: Component

Applicability

요청을 하는 소스와 그 요청을 실제 실행하는 객체를 decoupling하기 위해

decoupling: 분리

실행된 행위에 대한 undo 기능, 실행된 행위를 저장한 후 재실행하는 기능이 필요할 때

요청을 큐에 유지하고 나중에 실행할 필요성이 있을 때

장점

Command 객체를 여러 객체가 공유할 수 있음

실행 시간에 command와 receiver를 변경할 수 있음

새로운 command를 만들기 쉬움

Macro command를 만들기도 쉬움

728x90

'개발 일지 > 디자인패턴 & 아키텍쳐' 카테고리의 다른 글

템플릿 메소드 패턴  (0) 2021.12.29
어뎁터 패턴 & 파사드패턴  (0) 2021.12.29
싱글톤 패턴  (0) 2021.12.29
생성패턴  (0) 2021.12.29
장식자 패턴(Decorator)  (0) 2021.12.29