open:command-pattern

Command Pattern

중견 IT 서비스 회사 Serpent Hill & R.E.E는 미국 소비자들을 대상으로 하는 새로운 프로젝트를 수주했다. 첫 번째로 할 일은 새로운 사이트의 회원 가입과 로그인, 로그아웃을 처리하는 것이다.

페드로: 아! 그건 쉬워요. Command 인터페이스면 돼요.

interface Command {
  void execute();
}

페드로: 모든 명령 클래스에서 이 인터페이스를 구현하고 자신만의 execute 메소드를 정의하면 됩니다.

public class LoginCommand implements Command {

  private String user;
  private String password;

  public LoginCommand(String user, String password) {
    this.user = user;
    this.password = password;
  }

  @Override
  public void execute() {
    DB.login(user, password);
  }
}

public class LogoutCommand implements Command {

  private String user;

  public LogoutCommand(String user) {
    this.user = user;
  }

  @Override
  public void execute() {
    DB.logout(user);
  }
}

페드로: 사용법도 간단합니다.

(new LoginCommand("django", "unCh@1ned")).execute();
(new LogoutCommand("django")).execute();

페드로: 어떻게 생각해요, 이브?

이브: 바로 DB.login을 호출하지 않고, 굳이 LoginCommand 안에 감싸는 거죠?

페드로: 여기서 감싸는 것은 중요해요. 그래야 좀 더 다양한 Command 객체를 대상으로 작업을 수행할 수 있거든요.

이브: 왜 그래야 하죠?

페드로: 지연 호출이나, 로깅, 기록 추적, 캐싱 등 쓸 데가 아주 많죠.

이브: 알겠어요. 그럼 이건 어때요?

(defn execute [command]
  (command))
 
(execute #(db/login "django"  "unCh@1ned"))
(execute #(db/logout "django))

페드로: 이 해시(#) 기호는 도대체 뭐죠?

이브: 다음의 자바 코드의 단축형으로 보면 돼요.

new SomeInterfaceWithOneMethod() {
  @Override
  public void execute() {
    // do
  }
};

페드로: Command 인터페이스처럼 보이네요.

이브: 원한다면, 다음과 같이 해시 기호 없이 구현할 수도 있어요.

(defn execute [command & args]
  (apply command args))
  
(execute db/login "django" "unCh@1ned")

페드로: 이 경우에 지연 호출을 위해 함수를 저장하려면 어떻게 해야 하나요?

이브: 스스로 답해 보시죠. 함수를 호출하려면 무엇이 필요하죠?

페드로: 함수 이름…

이브: 그리고?

페드로: …함수의 인수들

이브: 맞았어요! 단순히 function-name과 arguments를 저장해 두었다가 언제든 원할 때 (apply function-name arguments) 형식으로 호출하면 돼요.

페드로: 음… 간단해 보이네요.

이브: 물론이죠. Command는 단순히 함수일 뿐이니까요

단일 메소드 인터페이스는 함수다.

위의 커맨드 패턴 예제에서 Command 인터페이스는 execute() 메소드 하나만 있는 인터페이스였다.
우리는 여기서ㅓ 메소드가 하나인 인터페이스, 즉 단일 메소드 인터페이스의 역할이 무엇인지 곰곰히 생각해 볼 필요가 있다
단일 메소드 인터페이스가 하는 일은 정확히 무엇일까?
이 인터페이스를 구현한 클래스의 인스턴스들에 대해서 그 인터페이스의 메소드(이 경우 Command 인터페이스의 execute)를 호출하기 위해서이다.
인스턴스들 즉 객체들은 자바에서는 함수의 파리미터로, 함수의 리턴 값으로, 멤버 변수나 지역 변수 그리고 배열이나 컨테이너 등에 저장할 수 있다.
자바는 객체지향 언어인 것이다.

인터페이스를 구현한 객체들을 전달해서 결국 그 인터페이스의 메소드를 호출하는 것이 목적인 것이다.
하지만 그것이 목적이라면 메소드 자체만을 전달하면 좋지 않을까?
왜 부질없이 객체를 다 전달하는가? 그저 우리는 그 객체의 특정 메소드를 호출하는 것이 관심일 뿐인데 말이다.
객체의 다른 부분들은 필요가 없는 것이다.
우리가 필요한 것은 객체가 아니라 함수다!

하지만 자바에서 함수는 1급이 아니다.
오로지 객체만이 파라미터로 리턴값으로, 변수로, 배열이나 컨테이너의 요소로서의 자격을 지닐 뿐이다.
자바에서는 함수를 그런 식으로 다룰 수는 없다.

만일 함수가 1급이라면 커맨드 패턴은 단지 커맨드 역할을 하는 함수를 전달하기만 하면 된다. 이것이 위의 에제에서 보았듯이 정확히 클로저가 하는 것이다.
클로저를 포함한 모든 함수형 언어에서는 다 마찬가지다.
자바에서는 이것이 불가능하기 때문에, 1급인 객체에 메소드를 매달아 전달한 후, 전달받은 측에서는 객체의 메소드를 불러내어 호출하는 것이다.

결국 단일 메소드 인터페이스는, 함수가 1급이 아니기 때문에, 1급인 객체에 함수를 매달아 전달하기 위한, 어쩔 수 없는 자바의 고육지책인 것이다.

커맨드 패턴 이외의 상당수의 디자인 패턴이 1급 함수를 이용하면 매우 단순하게 구현될 수 있다.

(인터페이스 구현에 있어서의 다형성의 측면이 여기서는 고려되지 않았는데, 이것은 전혀 다른 측면의 문제이기 때문이며, 클로저에서는 멀티 메소드라는 방식으로 자바보다 더 강력한 다형성을 지원한다)


  • open/command-pattern.txt
  • 마지막으로 수정됨: 2021/11/21 07:15
  • 저자 127.0.0.1