[사내 선배 개발자]분들 모두 잘하시지만(저는 언제쯤 잘할 수 있을까요? ㅜㅡㅜ), 

그 중 저에게 있어서 가장 믿을 수 있는 파트너였고, 간접적인 스승(직접적으로 뭔가 교육 등을 받지는 않았으니...)이신 선배님이 한 분 계십니다.


그 분과 디자인패턴 이야기가 종종 나올 때면, 언제나 끝에는 이 말로 종결을 짓곤 합니다.


"모든 디자인패턴은 Command 패턴을 구현하기 위해 존재할 뿐이야..."


이번 포스팅은 이 선배님께서 가장 강조하던 Command(명령)패턴에 관하여 다뤄보려고 합니다.


명령 패턴의 주 목적은 이름 그대로, 요청(명령)에 대한 캡슐화입니다.


이 패턴은 실제 요청자(Invoker)와 그 연산을 실제로 실행하는 자(Receiver) 사이에 일종의 중계자(Controller)를 두고 중계자가 캡슐화한 요청(명령)을 관리-실행을 하는 구조(has-a)로, 이는 요청자와 실행자 사이의 관계를 느슨하게 하는 것을 목표로 합니다.


언제나처럼 요구사항을 풀어가며 예제를 해결해보죠.



1. 요구사항


귀사에서 현재 제작하고 관리하는 어플리케이션 중 하나는 Cafe 관리 솔루션입니다.

Cafe 관리 솔루션은 "Cafe 내에서 사용하는 많은 infra 의 작동상태를 관리" 하는 것을 목적으로 하였으며, 꽤 많은 회사에서 이 솔루션을 사용하고 있습니다.


그렇기 때문에, Cafe 관리 솔루션의 요구사항은 끊기지 않는 것 같습니다. @.@


[욕심많은 기획자]는 다양한 기기를 관리할 수 있는 만능 리모콘 제작을 의뢰했습니다.

만능 리모콘에는 오직 한개의 버튼밖에 없지만, Cafe 에서 사용하는 많은 infra 의 on/off 를 오직 이 버튼만으로 관리하길 기대합니다.


여기서 중요한 것은 아직 우리는 관리할 모든 infra 를 받지 못했습니다. ㅜㅡㅜ



2. 다양한 infra, 더욱 다양한 서명들.


먼저 관리요청을 받은 infra 의 상태를 먼저 체크해 볼 필요가 있습니다.

각 infra 들은 많은 사람들에 의해서 제작이 되었고, 그렇기 때문에 당연하게도 메소드의 서명들이 모두 다릅니다.


또한, 버튼을 누를 때마다 해야할 일이 한 개는 아닌 것 같습니다.

Ex. MusicPlayer 의 경우, 버튼에 의해 활성화할 시 해야할 일은 아마도 MusicPlayer::on, MusicPlayer::startMusic 정도 일 것입니다.


하지만 더 안타까운 것은 추가 될 infra 역시 다양한 메소드 서명과 다양한 룰이 등장할 것이란거죠.



3. 요청의 캡슐화 및 컨트롤


메소드 서명과 사용 방법이 모든 infra 마다 다르다는 것은 꽤 골치 아픈 문제처럼 보입니다.

또한 만능 리모콘은 요청을 처리하기 위해서 infra 의 많은 정보를 알아야하는 것은 좋아 보이지 않습니다.

(만능 리모콘이 infra 가 추가될 때마다 해당 정보를 알기 위해 수정을 해야합니다.)


하지만 요청에 대한 일련의 복잡한 과정을 만능 리모콘에 맞게 '버튼'이라는 하나의 인터페이스로 단순화한다면 간단한 문제가 될 수 있을지도 모릅니다. 

단순화는 만능 리모콘이 요청을 처리하기 위해서 infra 의 정보를 몰라도 됨을 의미합니다.


우리는 이와같은 문제를 한 번 풀어본 적이 있습니다.

기억이 나나요? 맞습니다. 바로 Facade 의 개념을 도입해보죠.


Facade 패턴이 궁금하다면 아래 포스팅을 참고! :-)



만능 리모콘의 요청에 맞춰 각 infra가 해야할 일을 다음과 같이 단순화를 하고자 합니다.


1
2
3
4
5
6
7
8
9
10
11
/**
 * 리모콘이 할 수 있는 인터페이스 정의
 *
 * Created by Doohyun on 2017. 7. 6..
 */
public interface ICommand {
    /**
     * 실행
     */
    void execute();
}
cs



이에 따라 요청에 따라 infra 가 해야할 동작을 담을 ConcreteCommand 클래스를 만들어보죠.


아래는 MusicPlayer 와 MusicPlayer 에 대한 on/off 요청을 캡슐화한 클래스입니다.

요청을 캡슐화한 클래스에서는 요청전략에 따라 해야할 일련의 과정을 수행하도록 하였습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
 * 음악 플레이어
 *
 * <pre>
 *     실제 액션을 하는 객체를 의미
 *     receiver.
 * </pre>
 *
 * Created by Doohyun on 2017. 7. 6..
 */
public class MusicPlayer {
    
    public void on() {
        System.out.println("음악플레이어 전원을 올린다.");
    }
 
    public void startMusic() {
        System.out.println("음악 재생");
    }
 
    public void endMusic() {
        System.out.println("음악 끄기");
    }
    
    public void off() {
        System.out.println("음악플레이어 끄기");
    }
}
 
/**
 * 음악플레이어 스위치를 켜는 명령 정의.
 *
 * Created by Doohyun on 2017. 7. 6..
 */
public class MusicPlayerOnCommand implements ICommand {
 
    private MusicPlayer musicPlayer;
 
    public MusicPlayerOnCommand(MusicPlayer musicPlayer) {
        this.musicPlayer = musicPlayer;
    }
 
    /**
     * 음악 전원을 올리고 재생한다.
     */
    @Override
    public void execute() {
        musicPlayer.on();
        musicPlayer.startMusic();
    }
}
 
/**
 * 음악플레이어 스위치를 끄는 명령 정의.
 * 
 * Created by Doohyun on 2017. 7. 6..
 */
public class MusicPlayerOffCommand implements ICommand{
    private MusicPlayer musicPlayer;
 
    public MusicPlayerOffCommand(MusicPlayer musicPlayer) {
        this.musicPlayer = musicPlayer;
    }
 
    /**
     * 음악을 끄고, 전원을 내린다.
     */
    @Override
    public void execute() {
        musicPlayer.endMusic();
        musicPlayer.off();
    }
}
cs



이제 앞써 구현한 명령(ICommand) 객체를 사용할 만능리모콘은 다음과 같이 제작해보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
 * 명령 객체를 관리하는 중계자.
 *
 * Created by Doohyun on 2017. 7. 6..
 */
public class RemoteController {
 
    private ICommand command;
 
    /**
     * 명령객체 세팅.
     *
     * @param command
     */
    public void setCommand(ICommand command) {
        this.command = command;
    }
 
    /**
     * 실행
     */
    public void execute() {
        Optional.ofNullable(command).ifPresent(ICommand::execute);
    }
}
cs


RemoteController 는 ICommand 를 has-a 관계로 취하고 있으며, 이는 infra 를 실행함에 있어서 어떤 정보도 알 필요가 없음을 의미합니다.


즉, 캡슐화된 요청은 일종의 전략이라고 볼 수 있으며 RemoteController 는 전략을 사용하고 있는 형태로 볼 수 있을 것 같습니다.


전략 패턴에 대한 내용은 아래에서 확인할 수 있습니다.



이제 구현된 내용을 테스트하는 코드를 간단히 작성해보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Receiver
MusicPlayer musicPlayer = new MusicPlayer();
 
// Invoker 와 Receiver 사이의 Controller
RemoteController controller = new RemoteController();
 
// 음악플레이어 켜기. (invoke type 1)
controller.setCommand(new MusicPlayerOnCommand(musicPlayer));
controller.execute();
 
// 음악플레이어 끄기. (invoke type 2)
controller.setCommand(new MusicPlayerOffCommand(musicPlayer));
controller.execute();
 
// CONSOLE LOG
// 음악플레이어 전원을 올린다.
// 음악 재생
// 음악 끄기
// 음악플레이어 끄기
cs


이 구조에서는 만약 새로운 infra 가 생긴다 하더라도, 

ICommand 를 구현하는 요청객체를 만들어 RemoteController 에 세팅하면 특별히 다른 부분을 수정할 필요가 없어 보입니다. (OCP)


현재 구조를 UML 로 표현해보면 다음과 같습니다.



이 구조를 많이 보지 않았나요?

상위 모듈인 Controller 와 하위모듈인 ConcreteCommand 들은 모두 추상적인 ICommand 에 의존하고 있습니다. (DIP)


이 것에 대해 존경하는 또 다른 선배님의 말씀을 빌리면,


모든 패턴의 UML 을 그려보면 다 똑같아....


라고 하고 싶군요.



3. 명령 패턴 응용.


명령 패턴의 또 다른 묘미는 명령 객체를 관리할 수 있다는 것입니다.


Controller 에서 명령 객체를 has-a 관계로 유지하며 리하는 방식을 목적에 맞게 구현함으로써, undo/redo, macro 등을 개발해 볼 수 있습니다.


이러한 기능들은 특히, invoker 입장에서 특정 행위를 receiver 를 이용해 하기 위해 정보를 유지해야하는 불편함을 덜어줄 수 있을 것입니다.


이번 절에서는 명령 패턴의 대표적인 활용 예시인 UNDO/REDO 를 구현해보겠습니다.


이를 위해 앞써, 구현한 interface 의 명세를 조금 수정할 생각입니다.

특정 요청에 대한 행위를 취소하는 기능인 undo 를 추가하기 위해서죠..


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * undo 를 지원하는 명령인터페이스
 *
 * Created by Doohyun on 2017. 7. 6..
 */
public interface ICommand {
 
    /**
     * 실행취소
     */
    void undo();
 
    /**
     * 실행
     */
    void execute();
}
cs



이를 구현하는 ConcreteCommand 객체는 다음과 같습니다.

이번에는 특별하게 상태가 있는 요청입니다.


POSS 를 켜기 위해서는 id, pwd 가 필요하며, 요청객체에서 상태를 유지하도록 하였습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
 * 포스를 작동시키는 커맨드 구현.
 *
 * Created by Doohyun on 2017. 7. 7..
 */
public class POSSOnCommand implements ICommand {
 
    private String id;
    private String pwd;
    private POSS poss;
 
    /**
     * 포스를 켜는 커맨드 구현.
     *
     * <pre>
     *     상태를 유지함으로써, 
     *     invoker 는 요청을 한번 할 때를 제외하고는 해당 정보를 유지할 필요가 없음.
     * </pre>
     *
     * @param poss
     * @param id
     * @param pwd
     */
    public POSSOnCommand(POSS poss, String id, String pwd) {
        this.poss = poss;
        this.id = id;
        this.pwd = pwd;
    }
 
    @Override
    public void undo() {
        poss.logout();
        poss.closeSystem();
    }
 
    @Override
    public void execute() {
        poss.pushStartButton();
        poss.login(id, pwd);
    }
}
cs



다음 수정을 해볼 부분은 RemoteController 입니다. 

Stack 두 개를 목적에 따라 분류하여 명령 객체를 관리하고 있으며, RemoteController::execute, RemoteController::undo 가 실행될 때마다 적절하게 명령들을 이동시키고 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
 * 컨트롤러 undo/redo 지원
 *
 * Created by Doohyun on 2017. 7. 6..
 */
public class RemoteController {
 
    // 일반 명령을 위한 스택
    private Stack<ICommand> commandStack = new Stack<>();
    // UNDO 명령을 위한 스택
    private Stack<ICommand> undoStack = new Stack<>();
 
    // 명령을 추가
    public void setCommand(ICommand commandWithUndoable) {
        commandStack.push(commandWithUndoable);
    }
 
    /**
     * 일반 적인 실행. (REDO 포함)
     */
    public void execute() {
        if (!commandStack.isEmpty()) {
            // [일반명령 스택]에서 가장 마지막에 담긴 명령객체를 추출 후 실행.
            ICommand command = commandStack.pop();
            command.execute();
 
            // 해당 명령을 UNDO 스택에 삽입.
            undoStack.push(command);
        }
    }
 
    /**
     * 작업 취소 (Undo)
     */
    public void undo() {
        if (!undoStack.isEmpty()) {
            // [UNDO 명령 스택]에서 가장 마지막에 담긴 명령객체를 추출 후 실행.
            ICommand command = undoStack.pop();
            command.undo();
 
            // 일반 실행 스택에 데이터 삽입.
            commandStack.push(command);
        }
    }
}
 
cs



자, 코드가 수정 되었으니 마찬가지로 테스트 코드를 또 만들어보죠.

포스팅에서 제공은 안하지만 [커피머신 작동 명령] 까지 동시에 같이 세팅해 보도록 하였습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
CoffeeMachine coffeeMachine = new CoffeeMachine();
POSS poss = new POSS();
 
RemoteController controller2 = new RemoteController();
 
// 포스 작동 명령 세팅.
controller2.setCommand(new POSSOnCommand(poss, "Doohyun","486"));
 
controller2.execute();
controller2.undo();
controller2.execute();
 
// 커피머신 작동 명령 세팅
controller2.setCommand(new CoffeeMachineOnCommand(coffeeMachine));
controller2.execute();
controller2.undo();
controller2.undo();
controller2.execute();
 
// CONSOLE LOG
// 포스 켜기
// 로그인 : Doohyun, 486
// 로그아웃
// 포스 끄기
// 포스 켜기
// 로그인 : Doohyun, 486
// 커피기계 스위치 켜기
// 커피기계 스위치 끄기
// 로그아웃
// 포스 끄기
// 포스 켜기
// 로그인 : Doohyun, 486
cs


다행히, 실행/실행취소가 적절하게 잘 작동하는 것처럼 보입니다.

특히 주목할 점은 POSS 의 재실행 시, 상태인 id/pwd 를 다시 입력할 필요가 없다는 것입니다.

이는 invoker 입장에서 이와 같은 요구사항 처리 시, 정보를 계속 유지할 필요가 없음을 의미합니다.



이번 포스팅에서 다룬 명령 패턴은 '요청의 캡슐화' 라는 특정 목적을 가지고 있지만, 사실 여태 살펴본 다른 패턴들과 크게 다르지는 않은 듯 합니다.


특정 요청에 대한 복잡한 일련의 과정을 단순화한 전략에 따른 행위를 하도록 다형성을 이용했으며, 각 컴포넌트간의 관계를 느슨하게 위해 SOLID 의 두-세가지(SRP, OCP, DIP) 정도를 충족하도록 적용한 구조입니다.


패턴에 대한 이해도 중요하지만, 여러 패턴들 속에서 반복적으로 나타나는 이러한 특징들을 계속 접해보는 것도 도움이 되지 않을까 생각이 듭니다. :-)

감사합니다.



- 추가 사항 (2017.09. 10)


명령 패턴에 대한 실습자료는 아래와 같습니다. 감사합니다. ^^


CommandHomeWork.zip


반응형
Posted by N'