ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [디자인 패턴] 커맨드 패턴 (Command Pattern)
    IT, 프로그래밍/Design Patterns 2017. 12. 25. 20:43

    Command Design Pattern Class Diagram.png



    커맨드 패턴(Command pattern)을 이용하면, 요구사항을 객체로 캡슐화 할 수 있으며, 매개변수를 써서 여러가지 다른 요구사항을 집어넣을 수 도 있습니다. 또한 요청 내역을 큐에 저장하거나 로그로 기록할 수도 있으며, 작업취소 기능도 지원이 가능합니다.



    커맨드 패턴은 식당의 주문 시스템으로 이해 하면 쉽습니다.


    주문서로 주문을 하는 식당에 갔다고 생각을 해봅시다. 웨이터는 홀서빙 아르바이트생이라고 생각하셔도 좋습니다 ^^


    1. 자리에 앉아서 주문서를 적어서, 웨이터에게 전달합니다. 

    2. 웨이터는 주방에 가서 손님에게 받은 주문서를 전달합니다. 

    3. 주방장이 받은 주문대로 요리를 만듭니다. 


    주문서는 받은 주문을 캡슐화 하는 역할을 합니다. 웨이터는 주문서의 내용을 전혀 몰라도 됩니다. 그냥 이 주문서를 가져다가 그대로 주방에 가져다 주고 "주방장님 주문 들어왔어요!!" 하고 주방장을 호출 하면 됩니다. 즉 주방장과 웨이터는 완전히 분리되어 있다는 것이죠. (물론 현실에서의 웨이터는 알아야 되겠죠?)


    요리법에 대한 정보는 모두 주방장에게 있고, 주방장은 받은 주문서에 적혀 있는 대로 요리를 하면 되니, 웨이터와 주방장이 직접 대면해서 무언가를 할 필요가 없습니다.




    <커맨드 패턴으로 보는 식당 주문 시스템>



    식당 주문 시스템 

     커맨드 패턴

     손님

    클라이언트

    웨이터 

    인보커 객체 

    주문을 받는 것 

    setCommand() 

    주문서

    커맨드객체 

    주문을 주방장에게 전달하여 

    요리를 요청 하는 것. 

    execute()

    주방장

    리시버 객체 



    1. 클라이언트는 커맨드 객체를 생성 합니다. 


    2. 클라이언트에서 인보커 객체 안에 있는 setCommand() 메소드를 호출해서 커맨드 객체를 넘겨줍니다. 이 인보커 객체 안에 커맨드 객체가 쓰이기 전까지 보관 됩니다.  

    3. 인보커 객체에서 커맨드 객체의 execute() 메소드를 호출하면, 리시버에 있는 특정 행동을 하는 메소드가 호출 됩니다. 커맨드 패턴에서는 나중에 클라이언트에서 인보커에게 그 명령을 실행시켜 달라는 요청을 합니다.


    커맨드 객체가 제공하는 메소드는 execute() 메소드 하나 뿐이고, 이 안에는 행동과 리시버에 대한 정보가 같이 들어있습니다.  


    1
    2
    3
    4
    5
    6
        public void execute{
            
            receiver.action1();
            receiver.action2();
            
        }
    cs


    예를 들면 이런식으로 말이죠. 


    정확히 말하자면, 커맨드 패턴을 사용하는 식당 주문 시스템은, 손님이 웨이터에게 주문서를 전달해 놓고 먹고 싶을 때 웨이터에게 "요리 가져다 주세요!" 하고 말하는 것과 같습니다. 그러면 웨이터는 받아 놓은 주문서를 그제서야 주방장에게 가져다 주는 것이죠. 



    on/off 두 가지의 버튼으로 사물을 제어할 수 있는 리모컨이 있습니다. 이 리모컨을 커맨드 패턴으로 구현해 보겠습니다.




    간단한 리모컨 만들기 (전등 하나만 불을 켰다 끌 수 있는)



    1. Command 인터페이스 정의하기


    1
    2
    3
    4
    5
    public interface Command {
     
        public void execute();
        
    }
    cs


    커맨드 객체에는 execute() 메소드 하나밖에 지원 하지 않습니다. 하지만 나중에 여기에 작업 취소 기능을 위해 undo() 메소드를 추가할 수 있죠.


    2. 리시버 객체 (여기서는 전등) 만들기 


    1
    2
    3
    4
    5
    6
    public class Light {
     
        public void on() { System.out.println("전등 켜짐"); }
        public void off() { System.out.println("전등 꺼짐"); }
        
    }
    cs


    3. 인터페이스를 상속받는 커맨드 객체 만들기


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class LightOnCommand implements Command {
        Light light; 
            
        public LightOnCommand(Light light) {
            this.light = light;
        }
     
        public void execute() {
            light.on();
        }
    }
    cs



    4. 인보커 객체 만들기


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class SimpleRemoteControl {
        
        Command slot;
     
        public SimpleRemoteControl() {}
        
        public void setCommand(Command command) {slot = command;}
        
        public void buttonPressed() { slot.execute(); }
     
    }
    cs


    커맨드 객체를 저장하는 인보커 객체를 만듭니다. 클라이언트 객체에서 buttonPressed() 메소드를 호출하면 저장된 커맨드 객체에서 execute() 메소드를 호출하게 됩니다. 


    5. 클라이언트 객체 만들기 

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class RemoteControlTest {
        public static void main(String[] args) {
            SimpleRemoteControl remote = new SimpleRemoteControl();
            Light light = new Light();
            LightOnCommand lightOn = new LightOnCommand(light);
            
            remote.setCommand(lightOn);
            remote.buttonPressed();
        }
    }
    cs


    사용자는 커맨드 객체를 만들어, 인보커 객체인 SimpleRemoteControl 객체로 setCommand() 메소드를 통해 전달합니다. 그리고 클라이언트 객체가 buttonPressed() 메소드를 호출하면 execute() 메소드가 호출되면서, 전등의 on()메소드가 연달아 호출 되는 것이죠. 그렇게 되면 콘솔에 "전등 켜짐" 이라고 나타나게 됩니다. 






    복합 리모컨 만들기 

    (여러 개의 전등 및 가전기구를 컨트롤 할 수 있는 리모컨)


    1. Command 인터페이스 정의 하기


    1
    2
    3
    4
    5
    public interface Command {
     
        public void execute();
        
    }
    cs

    2. 리시버 객체 만들기 (전등 및 음악 플레이어)


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Light {
        
        private String location;
        
        public Light(String location) {
            this.location = location;
        }
        
        public void on() { System.out.println(location + " 전등 켜짐"); }
        public void off() { System.out.println(location +" 전등 꺼짐"); }
        
    }
    cs



    1
    2
    3
    4
    5
    6
    7
    public class MusicPlayer {
        
        public void on() { System.out.println("뮤직 플레이어 켜짐 및 최근 들은 곡 재생");}
            
        public void off() { System.out.println("뮤직 플레이어 꺼짐"); }
     
    }
    cs



    3. 커맨드 객체 만들기



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class LightOnCommand implements Command {
        Light light; 
            
        public LightOnCommand(Light light) {
            this.light = light;
        }
     
        public void execute() {
            
            light.on();
        }
    }
     
    cs


    전등 켜짐 커맨드


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class LightOffCommand implements Command {
        
        Light light; 
        
        public LightOffCommand(Light light) {
            this.light = light;
        }
     
        public void execute() {
            
            light.off();
        }
     
    }




    cs


    전등 꺼짐 커맨드


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class MusicPlayerOnCommand implements Command {
     
        
        private MusicPlayer musicPlayer;
        
        
        public MusicPlayerOnCommand(MusicPlayer musicPlayer) {
            this.musicPlayer = musicPlayer;
        }
        
        @Override
        public void execute() {
            // TODO Auto-generated method stub
            
            musicPlayer.on();
            
        }
     
        
        
    }
    cs


    음악 플레이어 켜짐 커맨드


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class MusicPlayerOffCommand implements Command {
     
        private MusicPlayer musicPlayer;
        
        public MusicPlayerOffCommand(MusicPlayer musicPlayer) {
            this.musicPlayer = musicPlayer;
        }
     
        @Override
        public void execute() {
            // TODO Auto-generated method stub
            
            musicPlayer.off();
            
        }
     
    }
    cs


    음악 플레이어 꺼짐 커맨드


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class NoCommand implements Command {
     
        @Override
        public void execute() {
            // TODO Auto-generated method stub
            
            System.out.println("명령 슬롯이 초기화 되어 있지 않습니다.");
            
        }
     
    }
    cs


    빈 명령 슬롯을 초기화 시키기 위한 Dummy 커맨드


    4, 인보커 객체 만들기


    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
    public class MultipleRemoteControl {
     
        Command[] onCommands;
        Command[] offCommands;
     
        public MultipleRemoteControl() {
     
            onCommands = new Command[7];
            offCommands = new Command[7];
            NoCommand noCommand = new NoCommand();
     
            for (int i = 0; i < 7; i++) {
     
                onCommands[i] = noCommand;
                offCommands[i] = noCommand;
     
            }
     
        }
     
        public void setCommand(int slot, Command onCommand, Command offCommand) {
            onCommands[slot]=onCommand;
            offCommands[slot]=offCommand;
        }
     
        public void onButtonPressed(int slot) {
            onCommands[slot].execute();
     
        }
        
        public void offButtonPressed(int slot) {
            
            offCommands[slot].execute();
            
        }
     
    }
    cs


    여기서 setCommand 메소드로 파라미터를 넘겨줄때는 slot 번호를 지정 해 주어야 합니다.


    5. 최종적으로 사용할 클라이언트 객체를 만듭니다.


    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
    public class RemoteControlTest {
        public static void main(String[] args) {
            MultipleRemoteControl remote = new MultipleRemoteControl();
            
            //리시버 및 커맨드 객체 생성
            Light livingLight = new Light("거실");
            LightOnCommand livingLightOn = new LightOnCommand(livingLight);
            LightOffCommand livingLightOff = new LightOffCommand(livingLight);
            
            Light kitchenLight = new Light("부엌");
            LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight);
            LightOffCommand kitchenLightOff = new LightOffCommand(kitchenLight);
            
            MusicPlayer musicPlayer = new MusicPlayer();
            MusicPlayerOnCommand musicPlayerOnCommand = new MusicPlayerOnCommand(musicPlayer);
            MusicPlayerOffCommand musicPlayerOffCommand = new MusicPlayerOffCommand(musicPlayer);
            
            //인보커 객체의 커맨드 배열에 커맨드 저장
            remote.setCommand(0, livingLightOn, livingLightOff);
            remote.setCommand(1, kitchenLightOn, kitchenLightOff);
            remote.setCommand(2, musicPlayerOnCommand, musicPlayerOffCommand);
            
            //인보커 객체 에서 커맨드 객체의 execute() 메소드 호출
            remote.onButtonPressed(0);
            remote.onButtonPressed(1);
            remote.onButtonPressed(2);
            
            remote.offButtonPressed(0);
            remote.offButtonPressed(1);
            remote.offButtonPressed(2);
            
            
            remote.onButtonPressed(4);
            remote.offButtonPressed(4);
        
        }
    }
    cs



    console : 


    거실 전등 켜짐

    부엌 전등 켜짐

    뮤직 플레이어 켜짐 및 최근 들은 곡 재생

    거실 전등 꺼짐

    부엌 전등 꺼짐

    뮤직 플레이어 꺼짐

    명령 슬롯이 초기화 되어 있지 않습니다.

    명령 슬롯이 초기화 되어 있지 않습니다.







    작업 취소 기능 만들기 (undo)


    1. 인터페이스에 undo 메소드 추가하기


    1
    2
    3
    4
    5
    6
    7
    public interface Command {
     
        public void execute();
        public void undo();
        
    }
     
    cs



    2. 커맨드 객체에 undo 메소드를 오버라이드 합니다.


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class LightOffCommand implements Command {
     
        Light light;
     
        public LightOffCommand(Light light) {
            this.light = light;
        }
     
        public void execute() {
     
            light.off();
        }
     
        public void undo() {
            
            System.out.println("-----작업 취소-----");
            light.on();
        }
     
    }
     
    cs


    이 커맨드 객체는 on 되어있는 전등을 off 시키는 객체이므로, 이 작업을 취소 한다면 다시 전등이 켜져야 할 것 입니다. 작업 취소를 확인하기 위해 작업 취소라는 문자열을 함께 넣었습니다.


    이런식으로 모든 커맨드 객체에 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
    47
    48
    public class MultipleRemoteControl {
     
        Command[] onCommands;
        Command[] offCommands;
        Command undoCommand;
     
        public MultipleRemoteControl() {
     
            onCommands = new Command[7];
            offCommands = new Command[7];
            NoCommand noCommand = new NoCommand();
     
            for (int i = 0; i < 7; i++) {
     
                onCommands[i] = noCommand;
                offCommands[i] = noCommand;
     
            }
            
            undoCommand = noCommand;
     
        }
     
        public void setCommand(int slot, Command onCommand, Command offCommand) {
            onCommands[slot]=onCommand;
            offCommands[slot]=offCommand;
        }
     
        public void onButtonPressed(int slot) {
            onCommands[slot].execute();
            undoCommand = onCommands[slot];
     
        }
        
        public void offButtonPressed(int slot) {
            
            offCommands[slot].execute();
            undoCommand = offCommands[slot];
            
        }
        
        public void undoButtonPressed() {
            
            undoCommand.undo();
            
        }
     
    }
    cs





    버튼이 눌릴때 마다 커맨드 객체를 undoCommand 변수에 저장합니다. 만약 클라이언트 객체에서 undo 버튼을 누른다면, 마지막으로 실행되었던 명령의 undo 메소드가 실행 될 것입니다. 


    결과를 볼까요?


    console : 


    거실 전등 켜짐

    부엌 전등 켜짐

    뮤직 플레이어 켜짐 및 최근 들은 곡 재생

    -----작업 취소-----

    뮤직 플레이어 꺼짐

    거실 전등 꺼짐

    부엌 전등 꺼짐

    뮤직 플레이어 꺼짐

    -----작업 취소-----

    뮤직 플레이어 켜짐 및 최근 들은 곡 재생

    명령 슬롯이 초기화 되어 있지 않습니다.

    명령 슬롯이 초기화 되어 있지 않습니다.

    명령 슬롯이 초기화 되어 있지 않습니다.






    매크로 커맨드 


    매크로 커맨드는, 여러개의 다른 명령들을 받아서 한번에 실행 시켜 주는 커맨드 객체 입니다.


    1. 매크로 커맨드 만들기


    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
    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();
            }
     
        }
     
        public void undo() {
     
            for (int i = 0; i < commands.length; i++) {
                commands[i].undo();
            }
     
        }
     
    }
     
    cs


    매크로 커맨드는 생성자를 통해 커맨드 배열을 넘겨받아, execute() 메소드가 호출되면 저장된 모든 명령을 한 번에 실행시켜 줍니다.


    2. 클라이언트에서 사용하기


    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
    public class RemoteControlTest {
        public static void main(String[] args) {
            MultipleRemoteControl remote = new MultipleRemoteControl();
            
            
            Light livingLight = new Light("거실");
            LightOnCommand livingLightOn = new LightOnCommand(livingLight);
            LightOffCommand livingLightOff = new LightOffCommand(livingLight);
            
            Light kitchenLight = new Light("부엌");
            LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight);
            LightOffCommand kitchenLightOff = new LightOffCommand(kitchenLight);
            
            MusicPlayer musicPlayer = new MusicPlayer();
            MusicPlayerOnCommand musicPlayerOnCommand = new MusicPlayerOnCommand(musicPlayer);
            MusicPlayerOffCommand musicPlayerOffCommand = new MusicPlayerOffCommand(musicPlayer);
            
            TV tv = new TV();
            TVOnCommand tvOnCommand = new TVOnCommand(tv);
            TVOffCommand tvOffCommand = new TVOffCommand(tv);
            
            Command[] partyOn = {livingLightOn, kitchenLightOn, musicPlayerOnCommand, tvOnCommand};
            Command[] partyOff = {livingLightOff, kitchenLightOff, musicPlayerOffCommand, tvOffCommand};
     
            MacroCommand partyOnMacro = new MacroCommand(partyOn);
            MacroCommand partyOffMacro = new MacroCommand(partyOff);
            
            remote.setCommand(0, partyOnMacro, partyOffMacro);
            
            remote.onButtonPressed(0);
            remote.undoButtonPressed();
            remote.offButtonPressed(0);
            
     
        }
    }
    cs


    파티를 할려면 부엌과 거실의 불을 켜고, tv와 음악 플레이어를 켜야 한다고 가정하겠습니다. 우리의 리모컨에 이 파티모드를 켜고 끌 수 있는 기능을 추가하려고 합니다. 


    우선 파티 모드 on에 해당하는 기능들을 모아 커맨드 배열로 만들고, 반대로 파티모드 off에 해당하는 기능들을 모아 배열로 만듭니다. 


    그리고 각각 매크로 커맨드 객체를 만들어, 파라미터로 넘겨주고


    setCommand를 통해 이 매크로 커맨드 객체를 슬롯에 저장합니다.


    다른 커맨드 객체를 할때와 동일한 수순으로 가는 것이죠. 


    실행 결과를 볼까요?


    console : 


    거실 전등 켜짐

    부엌 전등 켜짐

    뮤직 플레이어 켜짐 및 최근 들은 곡 재생

    TV 켜짐

    -----작업 취소-----

    거실 전등 꺼짐

    -----작업 취소-----

    부엌 전등 꺼짐

    -----작업 취소-----

    뮤직 플레이어 꺼짐

    -----작업 취소-----

    TV 꺼짐

    거실 전등 꺼짐

    부엌 전등 꺼짐

    뮤직 플레이어 꺼짐

    TV 꺼짐




    커맨드 패턴을 활용하면, 요청을 큐에 저장하여 스케줄러나 스레드 풀, 작업 큐 등에 응용할 수 있고 서버 프로그램에 활용한다면 로그를 통한 복구 시스템을 구축할 수도 있습니다. 

    댓글 1

    • 100su 2019.05.12 08:04

      정리 정말 잘해주셨네요.. 매크로 커맨드 부분을 이해를 못하고 있었는데 덕분에 알았습니다. 감사합니다.

Designed by Tistory.