커맨드는 무엇인가?
- 요청이나 메소드 호출을 객체로 나타내고 있다. 전달되는 파라미터의 정보와 실제 오퍼레이션은 커맨드 객체에 캡슐화되어 있다.
- 커맨드 패턴을 사용함으로써 얻는 이점은 메서드 호출이 나중에 실행되게끔 저장될 수 있는 객체이거나 코드의 일부에 전송되다는 것이다.
- 심지어 커맨드 객체에 큐(queue)를 사용하여 나중에 실행할 수도 있다.
UML
커맨드 구현
- 커맨드 인터페이스를 작성하면서 시작
- 커맨드를 실행하는 메소드를 정의해야 한다.
- 다음으로 각 요청에 대해서 인터페이스를 구현하거나 구현하고자 하는 오퍼레이션 유형을 구현한다. 커맨드는 또한 필요하다면 취소(undo) 오퍼레이션을 실행할 수도 있다.
- 각 구체 커맨드는 어떤 오퍼레이션이 필요한지 알고 있다. 필요한 것은 오퍼레이션의 파라미터와 실행되는 리시버 인스턴스이다.
- 클라이언트는 커맨드 인스턴스를 생성하고 리시버와 필요한 파리미터를 준비한다.
- 그리고 나면 커맨드 인스턴스가 전송될 준비가 된 것이다. 인보커는 커맨드 인스턴스를 사용하는 코드이고 커맨드 상에서 실행이 된다.
예제: UML
커맨드 인터페이스를 가지고 있다. 거기엔 단순한 execute 메서드를 가지고 있다. 커맨드 인터페이스에 대한 몇 개의 구현체를 가지고 있다. 예제에 대한 전반적인 전제는 웹서비스와 통신하고 이메일 서버, 메일 서버, 그리고 이메일 주소를 모아둔 서버가 있다. 메일 서버에서 생성되어 배분 목록으로 메일을 전송하고 그 목록의 구성원에게 메일이 전송된다.
EWSService는 리시버 객체이며 커맨드는 이 리시버의 메소드를 호출할 것이다.
예를 들어 AddMemberCommand 같은 구체 커맨드를 가지고 있다. 이 커맨드는 EWSService 서비스의 참조를 가지고 있다. 그래서 리시버에 대한 참조가 필요할 것이고 이메일 주소와 같은 인수가 필요하고 구현부를 실행한다. addMember메소드를 호출할 것이다.
Java 구현
public class EWSService {
public void addMember(String contact, String contactGroup) {
System.out.println("Added " + contact + " to " + contactGroup);
}
public void removeMember(String contact, String contactGroup) {
System.out.println("Removed " + contact + " from " + contactGroup);
}
}
EWService가 있고 더미클래스이며 커맨드를 수행하기 위한 Exchange Web Service를 연결한다. 그래서 addMember가 있고 emailAddress와 distribution list가 있다. emailAddress를 distributionList에 추가할 것이다.
이 클래스의 객체가 리시버 객체가 될 것이다. 오퍼레이션을 호출할 리시버가 필요하다.
public interface Command {
void execute();
}
다음으로 커맨드 인터페이스가 있다. 간단한 이유는 어떠한 인수도 받지 않는다. 왜냐하면 커맨드 구현에 필요한 모든 정보가 생성될 때 인스턴스에 주입되기 때문이다. 다른 이유는 인보커가 실제 커맨드를 실행한다는 것이다. 그래서 리시버가 어떤 것이며 인수가 어떤값이야 하는지 모른다.
public class AddMemberCommand implements Command {
private String emailAddress;
private String listName;
private EWSService receiver;
public AddMemberCommand(String email, String listName, EWSService service) {
this.emailAddress = email;
this.listName = listName;
this.receiver = service;
}
@Override
public void execute() {
receiver.addMember(emailAddress, listName);
}
}
public class MailTasksRunner implements Runnable {
private Thread runner;
private List<Command> pendingCommands;
private volatile boolean stop;
private static final MailTasksRunner RUNNER = new MailTasksRunner();
public static final MailTasksRunner getInstance() {
return RUNNER;
}
public MailTasksRunner() {
pendingCommands = new LinkedList<>();
runner = new Thread(this);
runner.start();
}
@Override
public void run() {
while(true) {
Command command = null;
synchronized (pendingCommands) {
if (pendingCommands.isEmpty()) {
try {
pendingCommands.wait();
} catch (InterruptedException e) {
System.out.println("Runner interrupted");
if (stop) {
System.out.println("Runner stopping");
return;
}
}
} else {
command = pendingCommands.remove(0);
}
}
if (command == null) {
return;
}
command.execute();
}
}
public void addCommand(Command cmd) {
synchronized (pendingCommands) {
pendingCommands.add(cmd);
pendingCommands.notifyAll();
}
}
public void shutdown() {
this.stop = true;
this.runner.interrupt();
}
}
이 클래스는 커맨드를 실행하는 인보커이다.
public class Client {
public static void main(String[] args) throws InterruptedException {
EWSService service = new EWSService();
Command c1 = new AddMemberCommand("a@a.com", "spam", service);
MailTasksRunner.getInstance().addCommand(c1);
Command c2 = new AddMemberCommand("b@b.com", "spam", service);
MailTasksRunner.getInstance().addCommand(c2);
Thread.sleep(3000);
MailTasksRunner.getInstance().shutdown();
}
}
구현 고려사항
- 커맨드에 언두(undo)&리두(redo)를 지원할 수 있다. 워크플로우 디자이너와 같은 복잡한 사용자 인터렉션을 가진 시스템에 유용하게 사용할 수 있다.
- 커맨드가 단순하다면, 예를 들면 언두(undo) 기능이 없고 어떤 상태도 가지지 않고 특정 기능과 인수를 사용하지 않는다면 동일한 유형의 요청에 대해 같은 커맨드 객체를 재사용할 수도 있다.
- 장기간동안 큐잉되는 커맨드에 대해서는 유지되는 상태의 수에 신경을 써야 한다.
설계 구현사항
- 커맨드는 코드의 일정부분을 재사용 하기 위해 다른 커맨드를 상속받을 수 있다.
- 다른 커맨드와 같이 커맨드를 구성할 수 있다. 이러한 "매크로" 커맨드는 요청을 처리하기 위해 실행되는 하나 이상의 서브 커맨드를 가질 것이다.
- 커맨드에서 언두(undo) 기능을 구현하는데 메멘토(memento) 디자인 패턴을 사용할 수 있고 이는 리시버에서 사용되는 내부 객체를 알 필요없이 리시버의 상태정보를 저장할 수 있게 된다.
위험 요소
- 리턴값과 에러 처리에 대해서는 논란의 여지가 생길 수 있다.
- 커맨드가 클라이언트와 커플링 없이 구현할 때 에러처리가 어렵다. 클라이언트가실행되는 리턴 값을 알 필요가 있을 때도 동일한 상황이 생긴다.
- 커맨드 패턴이 유용한 경우인, 인보커가 다른 쓰레드에서 실행중인 코드에서 에러 처리 & 리턴 값은 처리하기 더 복잡해진다.