Pattern - Command
공자께서 말씀하셨다. “배우고 그것을 때때로 익히면 기쁘지 (재미있지) 않겠는가.”
의도
요청 자체를 캡슐화하는 것입니다. 이를 통해 요청이 서로 다른 사용자를 매개변수로 만들고, 요청을 대기시키거나 로깅하며, 되돌릴 수 있는 연산을 지원합니다.
동기
- 항상 그렇지는 않지만, 요청받은 연산이 무엇이며, 이를 처리할 객체가 누구인지에 대한 아무런 정보 없이 임의의 객체에 메세지를 보내야 할 때가 간간이 있습니다. 예를 들어, 사용자 인터페이스 툴킷은 버튼, 메뉴 같은 객체를 포함하는데, 이는 사용자의 메세지드를 처리하게 됩니다. 그러나 사용자 인터페이스 툴킷은 버튼과 메뉴에서 요청을 처리할 수 없습니다. 툴킷을 사용하는 응용프로그랜만이 어떤 객체를 통해서 어떤 일이 되어야 하는지 알기 때문이다. 사용자 인터페이스 툴킷 설계자의 입장에서는 어떤 객체가 이 요청을 처리할 지를 알아낼 방법이 없습니다.
- 명령 패턴은 툴킷 객체가 요청 자체를 객체로 바꿈으로써 명시되지 않은 응용프로그램 객체의 요청을 처리할 수 있도록 지원하는 패턴입니다. 이 객체는 다른 객체 처럼 저장되거나 전달될 수도 있습니다. 이 패턴의 핵심은 연산을 실행하는데 필요한 인터페이스를 선언해 놓은 Command 클래스입니다. 이 클래스에 들어가는 가장 기본적인 연산이 execute()입니다. Command 추상 클래스에서 상속받은 Command 서브 클래스들은 수신 객체에 대한 참조자를 인스턴스 변수로 저장하며, 이 수신 객체에 정의된 요청을 호출하도록 Execute()를 구현하여 수신자-작동쌍을 정의합니다.
- Command 패턴은 연산을 호출하는 객체와 이를 수행하는 객체를 분리하고 있음을 알 수 있습니다. 이로써 사용자 인터페이스를 구현하는데 많은 융통성을 부여할 수 있습니다. 응용프로그램은 동일한 기능 처리에 메뉴를 사용할 수 있고 푸시 버튼을 사용할 수 있습니다. 단지 이들 인터페이스 요소가 Command를 상속하는 동일한 서브 클래스를 공유함으로써 동일하게 처리 되기 때문입니다. 또 명령어를 동적으로 교체할 수 있는데, 이것은 상황마다 다르게 메뉴를 구현해야 할 때 도움이 됩니다. 즉, 사용자 인터페이스는 동일한데, 선택시 처리되는 방식을 달리하려면 Command 를 상속하는 새로운 클래스만 정의하면 됩니다.
활용성
- 수행할 동작을 객체로 매개변수화하고자 할 때, 절차지향 프로그램에서는 이를 콜백함수, 즉 어딘가 등록되었다가 나중에 호출되는 함수를 사용해서 이러한 매개변수화를 표현할 수 있습니다. 명령 패턴은 콜백을 객체지향 방식으로 나타낸 것입니다.
- 서로 다른 시간에 요청을 명시하고, 저장하며, 실행하고 싶을 때, Command 객체는 원래의 요청과 다른 생명주기가 있습니다. 요청을 받아 처리하는 객체가 원래의 요청과 다른 생명주기가 있습니다. 요청을 받아 처리하는 객체가 주소 지정 방식과 독립적으로 표현될 수 있다면, Command 객체를 다른 프로세스에게 넘겨주고 거기서 해당 처리를 진행하게 할 수 있습니다.
- 실행 취소 기능을 지원하고 싶을 때, Command의 Execute() 연산은 상태를 저장할 수 있는데, 이를 이용해서 지금까지 얻은 결과를 바꿀 수 있습니다. 이를 위해 Unexecute() 연산을 Command 클래스의 인터페이스에 추가합니다. 실행된 명령어를 모두 기록해두었다가 이 리스트를 역으로 탐색해서 다시 Unexecute를 수행하게 됩니다. Execute()와 Unexecute() 연산의 반복 사용을 통해 수행과 취소를 무한 반복할 수 있습니다.
- 시스템이 고장났을때 재적용이 가능하도록 변경 과정에 대한 로깅을 지원하고 싶을 때, Command 인터페이스를 확장해서 load()와 source() 연산을 정의하면 상태의 변화를 지속적 저장소에 저장해 둘 수 있습니다. 시스템 장애가 발생했을 때 해당 저장소에서 저장된 명령어를 읽어 다시 Execute() 연산을 통해 재실행하면 됩니다.
- 기본적인 연산의 조합으로 만든 상위 수준 연산을 써서 시스템을 구조화하고 싶을 때. 정보 시스템의 일반적인 특성은 트랜잭션을 처리해야한다는 것입니다. 트랜잭션은 일련의 과정을 통해 데이터를 변경하는 것인데, Command 패턴은 이런 트랜잭션의 모델링이 가능하게 합니다. Command 클래스는 일관된 인터페이스를 정의하는데, 이로써 모든 트랜잭션이 동일한 방식으로 호출됩니다. 새로운 트랜잭션을 만들면 상속으로 Command 클래스를 확장하면 되므로 시스템 확장도 그리 어렵지 않습니다.
- 요청을 큐에 저장하기 큐 한 쪽 끝은 커멘드를 추가하고 다른 쪽 끝에는 커맨드를 처리하기 위한 스레드 들이 대기
- 요청을 로그에 기록하기 커맨더 패턴을 이용하면 Command 인터페이스에 store, load 라는 메소드를 추가하여 기능을 지원 가능하다. Invoker 클래스가 Command1, Command2, Command3.. 각각의 커멘드를 execute() 할 때 마다, 각 Command 들은 store()를 실행시켜 디스크에 각각의 객체를 저장한다. 컴퓨터가 다운되어 모든 작업이 멈추고 컴퓨터가 복귀되었을 때, 디스크로부터 각각의 Command1, Command2, … 작업하지 못한 객체들을 로딩하고 다시 순서대로 작업을 처리한다.
- 매크로 커맨드를 활용하는 방식 버튼 하나만으로도 여러 작업을 다수의 Command에 의해서 처리할 수 있다.
항목에 대한 설명
-
Command
연산 수행에 필요한 인터페이스를 선언합니다. -
ConcreteCommand
PasteCommand, OpenCommand
Receiver 객체와 액션 간의 연결성을 정의합니다. 또한 처리 객체에 정의된 연산을 호출하도록 Execute를 구현합니다. 앞에서 정의한 PasteCommand, OpenCommand를 예로 들 수 있습니다. -
Client
Application
ConcreteCommand 객체를 생성하고 처리 객체로 정의합니다. -
Invoker
MenuItem
명령어에 처리를 수행할 것을 요청합니다. -
Receiver
Document, Application
요청에 관련된 연산 수행 방법을 알고 있습니다. 어떤 클래스도 요청 수신자로 동작할 수 있습니다.
협력방법
- 사용자 ConcreteCommand 객체를 생성하고 이를 수신자로 지정합니다.
- Invoker 클래스는 Concrete Command 객체를 저장합니다.
- Invoker 클래스는 command에 정의된 Execute()를 호출하여 요청을 발생시킵니다. 명령이가 취소 가능한 것이라면 ConcreteCommand 는 이전에 Execute() 호출 전 상태의 취소 처리를 위해 저장합니다.
- ConcreteCommand 객체는 요청을 실제 처리할 객체에 정의된 연산을 호출합니다.
결과
- Command는 연산을 호출하는 객체와 연산 수행 방법을 구현하는 객체를 분리합니다.
- Command는 일급 클래스입니다. 다른 객체와 같은 방식으로 조작되고 확장할 수 있습니다.
- 명령을 여러 개를 복합해서 복합 명령을 만들 수 있습니다. 앞에서 Macro Command 클래스를 예로 들었지만, 복합체 패턴을 이용하여 여러 명령어를 구성할 수도 있습니다.
- 새로운 Command 객체를 추가하기 쉽습니다. 기존 클래스를 변경할 필요 없이 단지 새로운 명령어에 대응하는 클래스만 정의하면 됩니다.
코드 예제 – 기본 샘플
/*
문제를 해결하기 위해서는 구체적인 기능을 직접 구현하는 대신 실행될 기능을 캡슐화해야 한다.
즉, Button 클래스의 pressed 메서드에서 구체적인 기능(램프 켜기, 알람 동작 등)을 직접 구현하는 대신 버튼을 눌렀을 때
실행될 기능을 Button 클래스 외부에서 제공받아 캡슐화해 pressed 메서드에서 호출한다.
*/
package DesignPattern.gof_command.sample02;
public class Client {
public static void main(String[] args) {
Lamp lamp = new Lamp();
Command lampOnCommand = new LampOnCommand(lamp);
Alarm alarm = new Alarm();
Command alarmOnCommand = new AlarmStartCommand(alarm);
Button button1 = new Button(lampOnCommand);
button1.pressed();
Button button2 = new Button(alarmOnCommand);
button2.pressed();
button2.SetCommand(lampOnCommand);
button2.pressed();
}
}
package DesignPattern.gof_command.sample02;
public class Button {
private Command theCommand;
public Button(Command command ){
SetCommand(command);
}
public void SetCommand(Command command){
this.theCommand = command;
}
public void pressed(){
theCommand.execute();
}
}
package DesignPattern.gof_command.sample02;
public interface Command {
public abstract void execute();
}
package DesignPattern.gof_command.sample02;
public class Alarm {
public void start(){
System.out.println("Alarming");
}
}
package DesignPattern.gof_command.sample02;
public class AlarmStartCommand implements Command {
private Alarm theAlarm;
public AlarmStartCommand(Alarm alarm){
this.theAlarm = alarm;
}
@Override
public void execute() {
theAlarm.start();
}
}
package DesignPattern.gof_command.sample02;
public class Lamp {
public void turnOn(){
System.out.println("Lamp On");
}
}
package DesignPattern.gof_command.sample02;
public class LampOnCommand implements Command {
private Lamp theLamp;
public LampOnCommand(Lamp lamp){
this.theLamp = lamp;
}
@Override
public void execute() {
theLamp.turnOn();
}
}