[디자인패턴] 커맨드 패턴 (Command Pattern)

반응형

1. 커맨드 패턴 정의

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

커맨드 객체는 일련의 행동을 특정 리시버하고 연결시킴으로써 요구사항을 캡슐화한다. 이렇게 하기 위해 행동과 리시버를 한 객체에 집어넣고 메소드 하나만 외부에 공개하는 방법을 사용한다.

 

식당을 예로 들면

1. 손님이 웨이터에게 주문을 한다.

2. 점원이 고객의 주문을 주문서에 적는다.

3. 점원은 주문서를 주방에 전달하여 주문을 요청한다.

4. 요리사는 주문서에 적힌 주문대로 음식을 자신의 노하우로 만든다.

 

손님 = Client

점원 = Invoker 객체

주문서 = Command 객체

요리사 = Reciver 객체

주문을 하는 행위 = setCommand()

주문을 요청하는 행위 = execute()

 

2. 예제1 : 버튼이 한개만 존재하는 리모콘 (Remote Controller)

1. 커맨드 (command) 인터페이스

public interface Command {
    void execute();
}

2. 리시버 (receiver) 객체

// Light 리시버 객체 : 불을 키고 끄는 동작을 알고있다.
public class Light {
    public Light() {
    }
    public void on() {
        System.out.println("Light ON!"); // 불 켜는 동작
    }
    public void off() {
        System.out.println("Light OFF!"); // 불 끄는 동작
    }
}

3. 커맨드 (command) 인터페스를 구현하는 Concrete Class

// 불을 켜는 구현 커맨드 객체
public class LightOnCommand implements Command {
    Light light; // 리시버 객체 : 불을 키고 끄는 방법을 알고 있다.

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.on();
    }
}
// 불을 끄는 구현 커맨드 객체
public class LightOffCommand implements Command {
    Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.off();
    }
}

4. 호출자 (invoker 인보커) 객체 : 버튼이 하나밖에 없는 리모콘

public class RemoteControl {

    Command command; // 호출자(리모콘) 이 실행하는 명령(커맨드)

    public RemoteControl() {
    }

    public void setCommand(Command command) {
        this.command = command;
    }

    public void buttonWasPressed() {
        // 버튼을 누르면 명령을 실행한다
        command.execute(); 
    }
}

5. 클라이언트

@Test
void basic() {
    // 호출자 (Invoker)
    RemoteControl remoteControl = new RemoteControl();
    // 어떤 명령(command) 을 실행할지 리시버 (Receiver) 객체
    Light light = new Light();
    // 불을 켜는 명령(command)
    LightOnCommand onCommand = new LightOnCommand(light);
    // 호출자(invoker)에 명령 setting
    remoteControl.setCommand(onCommand);
    // 버튼 누르기 (명령 실행)
    remoteControl.buttonWasPressed();
    
    // 불을 끄는 명령 (command)
    LightOffCommand offCommand = new LightOffCommand(light);
    // 호출자(invoker)에 명령 setting
    remoteControl.setCommand(offCommand);
    // 버튼 누르기 (호출, 명령 실행)
    remoteControl.buttonWasPressed();
}

6. 실행결과 확인

Light ON!
Light OFF!

 

3. 예제2 : 버튼이 여러개 존재하는 리모콘 (Remote Controller)

1. 커맨드 (command) 인터페이스

public interface Command {
    void execute(); // 실행
    void undo(); // 실행취소
}

2. 리시버 (receiver) 객체

// 리시버 (Receiver) 1 : Light 객체
public class Light {
    String location;
    public Light(String location) {
        this.location = location;
    }
    public void on() {
        System.out.println("Light ON!");
    }
    public void off() {
        System.out.println("Light OFF!");
    }
}
// 리시버 (Receiver) 2 : Curtain 객체
public class Curtain {

    public static final int OPEN = 0;
    public static final int HALF_OPEN = 1;
    public static final int CLOSE = 2;

    String location;
    int level;

    public Curtain(String location) {
        this.location = location;
        level = OPEN;
    }
    
    public void open() {
        level = OPEN;
        System.out.println("커튼을 걷습니다.");
    }
    
    public void close() {
        level = CLOSE;
        System.out.println("커튼을 칩니다.");
    }

    public void halfOpen() {
        level = HALF_OPEN;
        System.out.println("커튼을 반만 칩니다.");
    }

    public int getLevel() {
        return level;
    }
}

3. 커맨드 (command) 인터페스를 구현하는 Concrete Class

// 전등 명령 (커맨드 , command) 구현 클래스 1 : ON
public class LightOnCommand implements Command {
    Light light; // 리시버 (receiver)
    public LightOnCommand(Light light) {
        this.light = light;
    }
    @Override
    public void execute() {
        light.on();
    }
    @Override
    public void undo() {
        light.off();
    }
}
// 전등 명령 (커맨드 , command) 구현 클래스 2 : OFF
public class LightOffCommand implements Command {
    Light light; // 리시버 (receiver)
    public LightOffCommand(Light light) {
        this.light = light;
    }
    @Override
    public void execute() {
        light.off();
    }
    @Override
    public void undo() {
        light.on();
    }
}
// 커튼 명령 (커맨드 , command) 구현 클래스 1 : OPEN
public class CurtainOpenCommand implements Command {
    Curtain curtain;
    int prevLevel;

    public CurtainOpenCommand(Curtain curtain) {
        this.curtain = curtain;
    }

    @Override
    public void execute() {
        prevLevel = curtain.getLevel();
        curtain.open();
    }
    
    @Override
    public void undo() {
        if (prevLevel == Curtain.OPEN) {
            curtain.open();
        } else if (prevLevel == Curtain.HALF_OPEN) {
            curtain.halfOpen();
        } else if (prevLevel == Curtain.CLOSE) {
            curtain.close();
        }
    }
}
// 커튼 명령 (커맨드 , command) 구현 클래스 2 : HALF OPEN
public class CurtainHalfOpenCommand implements Command {
    Curtain curtain;
    int prevLevel;

    public CurtainHalfOpenCommand(Curtain curtain) {
        this.curtain = curtain;
    }

    @Override
    public void execute() {
        prevLevel = curtain.getLevel();
        curtain.halfOpen();
    }

    @Override
    public void undo() {
        if (prevLevel == Curtain.OPEN) {
            curtain.open();
        } else if (prevLevel == Curtain.HALF_OPEN) {
            curtain.halfOpen();
        } else if (prevLevel == Curtain.CLOSE) {
            curtain.close();
        }
    }
}
// 커튼 명령 (커맨드 , command) 구현 클래스 3 : CLOSE
public class CurtainCloseCommand implements Command {
    Curtain curtain;
    int prevLevel;

    public CurtainCloseCommand(Curtain curtain) {
        this.curtain = curtain;
    }

    @Override
    public void execute() {
        prevLevel = curtain.getLevel();
        curtain.close();
    }

    @Override
    public void undo() {
        if (prevLevel == Curtain.OPEN) {
            curtain.open();
        } else if (prevLevel == Curtain.HALF_OPEN) {
            curtain.halfOpen();
        } else if (prevLevel == Curtain.CLOSE) {
            curtain.close();
        }
    }
}
/**
 * 실행명령이 없는 초기화용 구현 커맨드
 */
public class NoCommand implements Command {
    @Override
    public void execute() {}
    @Override
    public void undo() {}
}

4. 호출자 (invoker 인보커) 객체 : 버튼이 여러개인 리모콘

public class RemoteController {
    Command[] onCommands;
    Command[] offCommands;
    Command undoCommand; // 실행취소 버튼
    int buttonNum = 5; // 리모콘의 버튼 갯수

    public RemoteController() {
        onCommands = new Command[buttonNum];
        offCommands = new Command[buttonNum];

        Command noCommand = new NoCommand(); // 초기화용 커맨드 (실행 없음)
        for (int i = 0; i < buttonNum; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
        undoCommand = noCommand;
    }

    public void setCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand; // 지정 버튼(slot)에 on 셋팅
        offCommands[slot] = offCommand; // 지정 버튼(slot)에 off 셋팅
    }

    public void onButtonPush(int slot) {
        onCommands[slot].execute();
        undoCommand = onCommands[slot];
    }

    public void offButtonPush(int slot) {
        offCommands[slot].execute();
        undoCommand = offCommands[slot];
    }

    public void undoButtonPush() {
        undoCommand.undo();
    }

    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("\n------ Remote Control mapping info -------\n");
        for (int i = 0; i < onCommands.length; i++) {
            sb.append("[slot " + i + "] " + onCommands[i].getClass().getName()
                    + "    " + offCommands[i].getClass().getName() + "\n");
        }
        sb.append("[undo] " + undoCommand.getClass().getName() + "\n");
        return sb.toString();
    }
}

5. 클라이언트

 

@Test
void undoRemote() {
    // 명령 호출자 리모콘
    RemoteController remoteControl = new RemoteController();
    // 리시버 Light
    Light light = new Light("room light");
    // 명령 (command)
    LightOnCommand lightOnCommand = new LightOnCommand(light);
    LightOffCommand lightOffCommand = new LightOffCommand(light);
    // 커맨드 셋팅 (첫번째 버튼엔 전등 스위치)
    remoteControl.setCommand(0, lightOnCommand, lightOffCommand);

    // 리모콘으로 명령 실행
    remoteControl.onButtonPush(0);
    remoteControl.offButtonPush(0);
    System.out.println("== undo 버튼으로 실행취소 ==");
    remoteControl.undoButtonPush();
    System.out.println(remoteControl);

    // Curtain 리시버
    Curtain curtain = new Curtain("room curtain");
    // 명령 (command)
    CurtainOpenCommand curtainOpenCommand = new CurtainOpenCommand(curtain);
    CurtainHalfOpenCommand curtainHalfOpenCommand = new CurtainHalfOpenCommand(curtain);
    CurtainCloseCommand curtainCloseCommand = new CurtainCloseCommand(curtain);
    // 버튼에 명령 (command) 셋팅 : 두번째 버튼에는 커튼 열고 닫기
    remoteControl.setCommand(1, curtainOpenCommand, curtainCloseCommand);
    // 버튼에 명령 (command) 셋팅 : 세번째 버튼에는 커튼 반만 열거나 완전히 열기
    remoteControl.setCommand(2, curtainCloseCommand, curtainHalfOpenCommand);
    // 리모콘 명령 실행
    remoteControl.onButtonPush(1);
    remoteControl.offButtonPush(1);
    System.out.println("== undo 버튼으로 실행취소 ==");
    remoteControl.undoButtonPush();
    remoteControl.onButtonPush(2);
    remoteControl.offButtonPush(2);
    System.out.println("== undo 버튼으로 실행취소 ==");
    remoteControl.undoButtonPush();
    System.out.println(remoteControl);
}

6. 결과확인

Light ON!
Light OFF!
== undo 버튼으로 실행취소 ==
Light ON!

------ Remote Control mapping info -------
[slot 0] hello.example.designpattern.command.baiscExtend.LightOnCommand    hello.example.designpattern.command.baiscExtend.LightOffCommand
[slot 1] hello.example.designpattern.command.baiscExtend.NoCommand    hello.example.designpattern.command.baiscExtend.NoCommand
[slot 2] hello.example.designpattern.command.baiscExtend.NoCommand    hello.example.designpattern.command.baiscExtend.NoCommand
[slot 3] hello.example.designpattern.command.baiscExtend.NoCommand    hello.example.designpattern.command.baiscExtend.NoCommand
[slot 4] hello.example.designpattern.command.baiscExtend.NoCommand    hello.example.designpattern.command.baiscExtend.NoCommand
[undo] hello.example.designpattern.command.baiscExtend.LightOffCommand

커튼을 걷습니다.
커튼을 칩니다.
== undo 버튼으로 실행취소 ==
커튼을 걷습니다.
커튼을 칩니다.
커튼을 반만 칩니다.
== undo 버튼으로 실행취소 ==
커튼을 칩니다.

------ Remote Control mapping info -------
[slot 0] hello.example.designpattern.command.baiscExtend.LightOnCommand    hello.example.designpattern.command.baiscExtend.LightOffCommand
[slot 1] hello.example.designpattern.command.baiscExtend.CurtainOpenCommand    hello.example.designpattern.command.baiscExtend.CurtainCloseCommand
[slot 2] hello.example.designpattern.command.baiscExtend.CurtainCloseCommand    hello.example.designpattern.command.baiscExtend.CurtainHalfOpenCommand
[slot 3] hello.example.designpattern.command.baiscExtend.NoCommand    hello.example.designpattern.command.baiscExtend.NoCommand
[slot 4] hello.example.designpattern.command.baiscExtend.NoCommand    hello.example.designpattern.command.baiscExtend.NoCommand
[undo] hello.example.designpattern.command.baiscExtend.CurtainHalfOpenCommand

각각 버튼에 할당한 명령 (커맨드, command) 를 확인해보기 위해 Remote Control mapping info 를 출력해 보았다. 아래와 같은 형태로 출력되며 각 버튼에 할당된 정보를 확인할 수 있다.

[slot x] onCommand클래스정보    offCommand클래스정보

 

 

■ UML Class Diagram

 

 

 


참고
헤드 퍼스트 디자인 패턴 (Head First Design Patterns) - 에릭 프리먼,엘리자베스 롭슨,케이시 시에라,버트 베이츠

 

반응형