디자인패턴 - 커맨드 패턴(Command Pattern),매크로 커맨드 패턴(Macro Command Pattern)

2019. 8. 15. 23:00프로그래밍언어/디자인패턴

command의 의미는 '명령어'이다. 문서편집기의 복사(copy), 붙여넣기(paste), 잘라내기(cut) 등도 모두 명령어이다. 그런데 이런 명령어를 각각 구현하는 것보다는 [그림 5-52]처럼 하나의 추상 클래스 execute() 메서드를 하나 만들고 각 명령이 들어오면 그에 맞는 서브 클래스(copy_command)가 선택되어 실행하는 것이 효율적이다. 이는 함수 오버로딩(overloading)과 같은 추상화 개념을 사용한 것이다.

그러나 command 패턴은 단순히 명령어를 추상 클래스(abstract class)와 구체 클래스(copy_command,cut_command, paste_command)로 분리하여 단순화한 것으로 끝나지 않고, 명령어에 따른 취소(undo) 기능까지도 포함한다(사용자 입장에서는 해당 명령어를 실행했다가 취소(undo)하기도 하기 때문이다). 이렇게 프로그램의 명령어를 구현할 때는 command 패턴을 활용할 수 있다.

 

이러한 커맨드 패턴은 아주 예전에 사용하던 servlet에서도 사용하던 패턴입니다. FrontController를 최앞단에 두고 FrontController에서 모든 요청을 다 받은 후 커맨드 패턴을 이용하여 분기처리했습니다. 즉, 이렇게 어떠한 이벤트에 대해 실행될 기능이 다양하면서도 변경이 필요한 경우에 이벤트를 발생시키는 클래스를 변경하지 않고 재사용하고자 할때 유용한 패턴입니다.

 

만능 리모콘을 예제로 살펴보겠습니다. 리모콘은 실내의 전등을 키고 끄는 버튼이 있고 TV를 켜고 끄는 버튼도 있습니다. 추후에는 다양한 전자기기를 다룰 수 있는 버튼이 추가될 가능성이 있습니다. 이럴 경우에는 어떻게 커맨드 패턴을 적용해 볼 수 있을까요? 아래와 같이 쉽게 구현은 가능합니다.

 

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
public class SimpleRemoteController {
    
    private TV tv;
    private Light light;
    private Mode mode;
    
    enum Mode{
        TV,LIGHT;
    }
    
    public SimpleRemoteController(TV tv, Light light) {
        this.tv=tv;
        this.light=light;
    }
    
    public void setMode(Mode mode) {
        this.mode=mode;
    }
    
    public void pressOnButton() {
        if(this.mode.equals(Mode.TV)) {
            tv.turnOn();
        }else if(this.mode.equals(Mode.LIGHT)) {
            light.turnOff();
        }
    }
    
    ...
    
}
cs

이 코드에는 분명 문제가 있습니다. SOLID 원칙 중 분명 OCP에 위반되기 때문이죠. 확장에는 열려있고 변경에는 닫혀있어야 하는 원칙을 누가 봐도 어기고 있습니다. 기능이 추가될때마다 코드 변경이 필요하기 때문이죠. 하지만 이것을 커맨드 패턴을 이용해 구현한다면 리모콘을 컨트롤하는 클래스는 어떠한 행위에 대한 구체적인 구현을 몰라도 되며 구체적인 행동을 캡슐화하고 있는 커맨드 객체만 받으면 되기에 훨씬 확장이 수월한 코드가 나오게 됩니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RemoteControlInvoker {
    
    private Command command;
    
    public RemoteControlInvoker(Command command) {
        this.command=command;
    }
 
    public void setCommand(Command command) {
        this.command = command;
    }
    
    public void pressedButton() {
        command.execute();
    }
    
}
cs

 

이제는 리모콘 역할을 하는 클래스에는 구체적인 행위가 정의되어 있지 않습니다. 생성자의 매개변수 혹은 setter를 이용하여 주입받은 커맨드 객체로 행위를 호출하는 호출자(Invoker) 역할을 하게됩니다.

 

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
public interface Command {
    public void execute();
}
 
public class TvOnCommand implements Command {
    
    private TV tv;
    
    public TvOnCommand(TV tv) {
        this.tv=tv;
    }
    
    @Override
    public void execute() {
        tv.turnOn();
    }
 
}
 
public class TvOffCommand implements Command {
    
    private TV tv;
    
    public TvOffCommand(TV tv) {
        this.tv=tv;
    }
    
    @Override
    public void execute() {
        tv.turnOff();
    }
 
}
 
public class LightOnCommand implements Command {
    
    private Light light;
    
    public LightOnCommand(Light light) {
        this.light=light;
    }
    
    @Override
    public void execute() {
        light.turnOn();
    }
 
}
 
public class LightOffCommand implements Command {
    
    private Light light;
    
    public LightOffCommand(Light light) {
        this.light=light;
    }
    
    @Override
    public void execute() {
        light.turnOff();
    }
 
}
 
cs

 

Command라는 인터페이스를 만들고 execute()라는 메소드를 선언합니다. 그리고 구체적인 요청(행위)을 하나의 커맨드 객체로 캡슐화 합니다. 해당 커맨드 객체는 요청을 커맨드 객체에게 간접적으로 받아서 처리하는(Receiver)를 한번 감싸는 행위 캡슐화 역할을 하게 됩니다.

특이하게 이제는 클라이언트 입장에서 어떠한 행위든지 상관없이 execute()만 호출하면 되는 것입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TV {
    
    public void turnOn() {
        System.out.println("TV 켰음");
    }
    public void turnOff() {
        System.out.println("TV 껐음");
    }
}
 
public class Light {
    
    public void turnOn() {
        System.out.println("불 켰음");
    }
    public void turnOff() {
        System.out.println("불 껐음");
    }
}
 
 
cs

 

리시버 객체의 구현입니다.

 

이제 리모콘 역할을 하는 Invoker는 어떠한 기능이 추가되더라도 코드 변경없이 기능을 얼마던지 추가 할 수 있습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class CommandMain {
 
    public static void main(String[] args) {
        
        TV tv = new TV();
        TvOnCommand tvOnCommand = new TvOnCommand(tv);
        TvOffCommand tvOffCommand = new TvOffCommand(tv);
        
        Light light = new Light();
        LightOnCommand lightOnCommand = new LightOnCommand(light);
        LightOffCommand lightOffCommand = new LightOffCommand(light);
        
        RemoteControlInvoker invoker = new RemoteControlInvoker(tvOnCommand);
        invoker.pressedButton();
        invoker.setCommand(tvOffCommand);
        invoker.pressedButton();
        invoker.setCommand(lightOnCommand);
        invoker.pressedButton();
        invoker.setCommand(lightOffCommand);
        invoker.pressedButton();
        
    }
 
}
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class MacroCommand implements Command {
    
    private List<Command> commands;
    
    public MacroCommand() {
        commands= new ArrayList<>();
    }
    
    public void addCommand(Command...commands) {
        if(commands.length < 1throw new NullPointerException();
        
        for(Command c : commands) {
            this.commands.add(c);
        }
    }
    
    @Override
    public void execute() {
        if(commands.size()>0) {
            commands.stream().forEach(c->c.execute());
        }
    }
 
}
 
public class CommandMain {
 
    public static void main(String[] args) {
        
        TV tv = new TV();
        TvOnCommand tvOnCommand = new TvOnCommand(tv);
        TvOffCommand tvOffCommand = new TvOffCommand(tv);
        
        Light light = new Light();
        LightOnCommand lightOnCommand = new LightOnCommand(light);
        LightOffCommand lightOffCommand = new LightOffCommand(light);
        
        MacroCommand macroCommand = new MacroCommand();
        macroCommand.addCommand(tvOnCommand,
                                tvOffCommand,
                                lightOnCommand,
                                lightOffCommand);
        RemoteControlInvoker invoker = new RemoteControlInvoker(macroCommand);
        invoker.pressedButton();
        
    }
 
}
cs

 

여기까지 간단한 커맨드 패턴 예제를 다루어봤습니다. 사실 커맨드 패턴은 위보다도 더욱 복잡하게 사용가능한 패턴입니다. 예를 들어 상태값을 가져 상태에 따른 행위를 할 수 있고, 또한 확장하여 일련의 작업들을 작업 큐에 넣어 쓰레드들이 각각 하나의 Command를 맡아서 작업을 할 수도 있는 등 구현의 범위는 넓습니다.