[디자인 패턴] 커맨드 패턴

이정규·2022년 6월 21일
0

리모컨이 있다고 해보자.

이 리모컨으로 TV를 켜보는 것을 구상해보자.

public class TV {
    public void turnOn() {
        System.out.println("tv가 켜졌습니다.");
    }
}
public class RemoteController {
    TV tv;

    public RemoteController(TV tv) {
        this.tv = tv;
    }

    public void turnOn() {
        tv.turnOn();
    }
}
public class Main {
    public static void main(String[] args) {
        TV tv = new TV();
        RemoteController controller = new RemoteController(tv);

        controller.turnOn();
    }
}

그런데 이 리모컨은 만능이라서 TV말고도 전등도 킬 수 있다.

전등을 킬 수 있게도 구상해보자.

public class Light {
    public void turnOn() {
        System.out.println("전등이 켜졌습니다.");
    }
}
public class RemoteController {
    TV tv;
    Light light;
    String mode;
    String[] modes = {"tv", "light"};
    public RemoteController(TV tv, Light light) {
        this.tv = tv;
        this.light = light;
    }

    public void setMode(int idx) {
        mode = modes[idx];
    }

    public void turnOn() {
        switch (mode) {
            case "tv":
                tv.turnOn();
                break;
            case "light":
                light.turnOn();
                break;
        }
    }
}
public class Main {
    public static void main(String[] args) {
        TV tv = new TV();
        Light light = new Light();
        RemoteController controller = new RemoteController(tv, light);

        controller.setMode(0);
        controller.turnOn();

        controller.setMode(1);
        controller.turnOn();
    }
}

만능 리모컨이 킬 수 있는 물건(객체)가 늘어날 때마다 코드가 변경이 일어난다.

⇒ OCP위배가 된다는 말이다.

이를 커맨드 패턴을 이용해 해결해보자.

커맨드 패턴이란 다음과 같다.

정의

요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화할 수 있다. 이러면 요청을 큐에 저장하거나 로그로 기록하거나 작업 취소 기능을 사용할 수 있다.

구성

커맨드 패턴은 3가지로 구성이 되어 있다.

  1. 커맨드
  2. 리보커
  3. 리시버

이렇게 딱딱하게 말하면 어렵다. 음식점에 비유를 해보자.

우리가 레스토랑에서 메뉴판을 보고 종업원에게 “스테이크 1개, 와인 1개 주세요.” 라고 주문을 한다.

그러면 종업원은 주문서에 해당 주문을 적고 이를 주방장에게 갖다준다.

주방장은 주문서에 적힌 주문을 보고 요리를 시작한다.

이를 보면 어떤 것을 요구하는 객체, 그 요구를 받아들이고 처리하는 객체로 나뉘어진 것을 볼 수 있다.

다시 3가지 구성으로 돌아가보자.

  1. 커맨드

    위의 예시에서 주문서에 해당한다. 커맨드를 전달하는 것이다.

  2. 리보커

    위의 예시에서 종업원에 해당한다. 그저 커맨드를 받아 전달하는 위치이다.

  3. 리시버

    위의 예시에서 주방장에 해당한다. 리보커에게 전달받은 커맨드를 보고 수행할 뿐이다.

예시

Light, TV는 위와 같다.

Command

public interface Command {
    void execute();
}
public class LightOnCommand implements Command {
    Light light;

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

    @Override
    public void execute() {
        light.turnOn();
    }
}
public class TVOnCommand implements Command {
    TV tv;

    public TVOnCommand(TV tv) {
        this.tv = tv;
    }

    @Override
    public void execute() {
        tv.turnOn();
    }
}

RemoteController

public class RemoteController {
    Command command;

    public void setCommand(Command com) {
        command = com;
    }

    public void buttonWasPressed() {
        command.execute();
    }
}

Test

public class Main {
    public static void main(String[] args) {
        TV tv = new TV();
        Light light = new Light();
        Command lightOn = new LightOnCommand(light);
        Command tvOn = new TVOnCommand(tv);

        RemoteController controller = new RemoteController();

        controller.setCommand(lightOn);
        controller.buttonWasPressed();

        controller.setCommand(tvOn);
        controller.buttonWasPressed();
    }
}

다이어그램

위 그림을 보면 Invoker가 커맨드를 지정해주고, 지정한 커맨드를 Receiver가 실행만 하는 구조로 되어있다.

이렇게 Invoker, Receiver, Command가 캡슐화 되어있어 결합도가 낮아져 유지보수가 용이해진다.

위에서 정의내용에서 작업내용을 취소할 수 도 있게 만들 수 있다고 했는데 한 번 만들어보자.

Command

public interface Command {
    void execute();
    void undo();
}
public class LightOnCommand implements Command {
    Light light;

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

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

    @Override
    public void undo() {
        light.turnOff();
    }
}
public class LightOffCommand implements Command {
    Light light;

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

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

    @Override
    public void undo() {
        light.turnOn();
    }
}

RemoteController

public class RemoteController {
    Command command;
    Command undoCommand;

    public RemoteController() {
        Command noCommand = new NoCommand();
        command = noCommand;
        undoCommand = noCommand;
    }

    public void setCommand(Command com) {
        command = com;
    }

    public void buttonWasPressed() {
        command.execute();
        undoCommand = command;
    }

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

Test

public class Main {
    public static void main(String[] args) {
        TV tv = new TV();
        Light light = new Light();
        Command lightOn = new LightOnCommand(light);
        Command tvOn = new TVOnCommand(tv);

        RemoteController controller = new RemoteController();

        controller.setCommand(lightOn);
        controller.buttonWasPressed();

        controller.setCommand(tvOn);
        controller.buttonWasPressed();
        controller.undoButtonWasPressed();
    }
}

다음과 같이 진행하면 된다. 간단하게 만들었는데 만약 undo버튼이 여러번 눌리도록 하고 싶다면 stack으로 저장하여 진행하면 된다.

사용 이유

  1. 명령을 만드는 시점과 실행시키는 시점이 다르다.
  2. 명령을 만드는 친구와 실행시키는 친구가 다르다.
  3. 작업의 취소 또는 재실행등을 할 수 있게 하기 위함이다.

참고
https://victorydntmd.tistory.com/295
https://soojong.tistory.com/entry/디자인패턴-커맨드-패턴Command-Pattern
http://egloos.zum.com/iilii/v/5378691
헤드퍼스트 디자인 패턴

profile
강한 백엔드 개발자가 되기 위한 여정

0개의 댓글