도서 요약 / / 2023. 2. 27. 20:22

커맨드(command) 패턴

커맨드는 무엇인가?

  • 요청이나 메소드 호출을 객체로 나타내고 있다. 전달되는 파라미터의 정보와 실제 오퍼레이션은 커맨드 객체에 캡슐화되어 있다.
  • 커맨드 패턴을 사용함으로써 얻는 이점은 메서드 호출이 나중에 실행되게끔 저장될 수 있는 객체이거나 코드의 일부에 전송되다는 것이다.
  • 심지어 커맨드 객체에 큐(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) 디자인 패턴을 사용할 수 있고 이는 리시버에서 사용되는 내부 객체를 알 필요없이 리시버의 상태정보를 저장할 수 있게 된다.

위험 요소

  • 리턴값과 에러 처리에 대해서는 논란의 여지가 생길 수 있다.
  • 커맨드가 클라이언트와 커플링 없이 구현할 때 에러처리가 어렵다. 클라이언트가실행되는 리턴 값을 알 필요가 있을 때도 동일한 상황이 생긴다.
  • 커맨드 패턴이 유용한 경우인, 인보커가 다른 쓰레드에서 실행중인 코드에서 에러 처리 & 리턴 값은 처리하기 더 복잡해진다.



반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유