이번 장에서는 한 차원 높은 단계의 캡슐화를 다루는데,

바로 메소드 호출을 캡슐화하는 것이다. 


이를 활용해서 스케줄링, 로그 기록 시스템을 디자인할 수도 있고

취소(undo) 기능을 구현하기 위해 재사용할 수도 있다.


* Command Pattern :

요구 사항을 객체로 캡슐화 할 수 있으며, 매개변수를 써서 여러가지 다른 요구 사항을 집어넣을 수도 있다.

또한 요청 내역을 큐에 저장하거나 로그로 기록할 수도 있고 작업 취소 기능도 지원이 가능하다.

요청을 하는 객체그 요청을 수행하는 객체를 분리하고 싶다면 이 패턴을 사용하자!



Command 패턴을 이해하기 위해 간단하게 예를 들어보겠다.

다음의 그림은 식당에서 클라이언트의 주문이 실제 수행되는 과정이다.

(출처 : http://www.prayer-laputa.com/blog/archives/370)


웨이트리스는 클라이언트로부터 주문(Order)를 받아 - takeOrder()

주방장에게 그 주문을 전달해준다. - orderUp()

그러면 주방장은 주문서에 따라서 음식을 준비할 것이다. - make...()


여기서 주문서가 주문한 메뉴를 캡슐화 한다는 것을 숙지하자.


그리고 위 그림을 읽는 방법을 설명하자면

1) Client 는 Order 의 createOrder() 를 호출.

2) Order 는 웨이트리스의 takeOrder() 를 호출.

3) 웨이트리스는 Order 의 orderUp() 을 호출.

4) Order 는 주방장의 make...() 을 호출.

- 3) 이 이해가 잘 안 갈 수도 있지만 너무 신경안써도 된다. 흐름만 이해하자.



위 그림을 어느 정도 이해한 다음에

Command 패턴 다이어그램으로 넘어가보자.


위에서 보았던 다이어그램과 매우 유사하다는 것을 느낄 수 있다.


이해를 위해 설명을 하자면,

① Client 는 Command 객체를 생성한다.

② Invoker 에 ① 에서 생성한 Command 객체를 저장하기 위해 Client 는 setCommand() 를 호출한다.

③ 차후 Client 는 Invoker 에게 그 Command 객체를 실행시켜달라고 요청한다.




- Command 패턴의 흐름을 이해했다면 예제 코드로 넘어간다.

책에서는 가정 내에서 사용하는 전자제품들을 컨트롤할 수 있는 리모컨 API 를 디자인하는 예를 들었다.

아래 Class Diagram 안 보이면 Click.

(참고로 나는 리모컨에 Undo 기능이 있는 건 처음본다)


Client :

Concrete Command 를 생성하고 Receiver를 설정한다.


- Invoker :

Command 들을 관리하고 있고 Command 의 execute() 를 호출해서 Command 객체에게

특정 작업을 수행해 달라고 요청을 한다.


- Command :

모든 Command 객체에서 구현되어야 하는 인터페이스.


- Concrete Command :

execute() 에서 Receiver 에 있는 Action 들을 호출해 요청받은 작업을 수행하도록 한다.

undo() 의 예를 들면, LightOnCommand 의 경우 undo() 의 내용은 light.off() 정도 될 것이다.


- Receiver :

요구 사항을 처리하기 위해 어떤 일을 수행해야 하는지 알고 있다.


- Macro Command :

여러 Command 들을 한번에 실행시킬 수 있는 새로운 종류의 Command. 

아래의 예제 코드를 보자.

public class MacroCommand implements Command {
    Command[] commands;
    public MacroCommand(Command[] commands){
        this.commands = commands;
    }
    public void execute() {
        for ( int i=0; i < commands.length; i++ ){
            commands[i].execute();
        }
    }
}

- Null 객체 :

Client 에서 따로 null 처리를 하지 않아도 되도록 만들어 놓은 객체.

execute(), undo() 모두 아무 일도 하지 않는다. 여러 디자인 패턴에서 유용하게 쓰인다고 한다.

public void onButtonWasPushed(int slot) {
    if ( onCommands[slot] != null ) {
         onCommands[slot].execute(); 
    }
}

Null 객체를 활용하면 클라이언트는 위의 코드처럼 작성할 필요가 없게된다.



Invoker 인 RemoteControl 클래스의 코드를 자세히 알고 싶다면 

이 포스팅 글 상단에 걸어놓은 링크를 따라가면 된다. 중국어의 압박이 있긴하지만 코드는 읽을 수 있으니까.



* Command 패턴 활용 :

(1) 요청을 Command 객체에 캡슐화해서 작업 큐(Queue) 에 저장하기

큐의 한쪽은 커맨드를 추가, 다른 한쪽은 커맨드를 처리하기 위한 스레드(Thread)들이 대기하고 있다고 하자.

각 스레드에서는 우선 커맨드의 execute() 메소드를 호출하고, 이 작업을 완료하면 커맨드 객체를

보내버리고 새로운 커맨드 객체를 가져오는 작업을 하게 된다.


위와 같은 방법으로 스케줄러, 스레드 풀, 웹 서버 등에 활용할 수 있다.


(2) 요청을 로그(Log)에 기록하기

어떤 애플리케이션에서는 모든 행동을 기록해놨다가 그 애플리케이션이 다운되었을 경우,

나중에 그 행동들을 다시 호출해서 복구를 할 수 있도록 해야 한다.

이를 위해 Command 인터페이스에 execute(), undo() 이외에 store(), load() 를 추가하자.


어떤 명령을 실행하면서 디스크에 실행 히스토리를 기록해 애플리케이션이 다운되면

커맨드 객체를 다시 로딩하고 execute() 메소드들을 자동으로 순서대로 실행하면 된다.


매번 저장하기 힘든 경우에는 마지막 체크 포인트 이후로 한 모든 작업을 저장한 다음에

다운되었을 때 기존 체크 포인트에 최근 수행된 작업을 다시 적용하는 방법을 사용하면 된다.


위와 같은 테크닉을 확장해서 DB의 트랜잭션을 활용해 commit, rollback 연산을 구현할 수 있을 것이다.



* 객체지향의 원칙 :

1) 바뀌는 부분은 캡슐화 한다.

2) 상속보다는 구성을 활용한다.

3) 구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.

4) 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.

5) 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다(OCP).

6) 추상화된 것에 의존해라. 구상 클래스에 의존해서는 안 된다(의존성 뒤집기 법칙).



* 이 장에서의 정리 :

- Command 패턴을 이용하면 요청을 하는 객체와 그 요청을 수행하는 객체를 분리할 수 있다.

- 또한 작업 취소(Undo) 기능을 지원할 수 있고, 로그나 트랜잭션 시스템 구현에도 활용된다.

- Command 객체는 Action 을 수행하는 Receiver 를 캡슐화 한다.

- Command 객체의 execute() 는 Receiver 의 Action 을 호출한다.

- Macro Command 는 여러 개의 Command 를 한꺼번에 호출할 수 있게 해주는 간단한 방법이다.

Macro Command 에서도 어렵지 않게 Undo() 기능을 지원할 수 있다.


by kelicia 2014. 5. 21. 20:46