관리 메뉴

SW

[개발서적] 헤드퍼스트 디자인 패턴 Ch6. 커맨드(Command) 패턴 본문

카테고리 없음

[개발서적] 헤드퍼스트 디자인 패턴 Ch6. 커맨드(Command) 패턴

SWKo 2024. 8. 15. 12:23

1. 커맨드 패턴 소개

커맨드 패턴은 요청을 객체로 캡슐화하여 서로 다른 요청을 매개변수화하거나, 요청의 취소 및 재실행 등을 가능하게 하는 디자인 패턴입니다. 이 패턴을 사용하면 요청을 처리하는 객체(Receiver)와 요청을 발행하는 객체(Client)를 분리할 수 있습니다.

처음에 말만 들어서는 이해가 잘 안가니 아래 내용을 따라가 보겠습니다.

 

객체 마을 식당 예시

  • 고객: 요청을 발행하는 역할 (커맨드 객체를 생성).
  • 종업원: 커맨드 객체를 전달하는 역할.
  • 주방장: 커맨드 객체에 따라 실제로 작업을 수행하는 역할.

 

  • 주문서 (Command 객체):
    • 역할: 주문서 객체는 주문 내용을 캡슐화합니다. 이는 요청을 특정한 형식으로 묶어서 다른 객체에게 전달할 수 있도록 하는 역할을 합니다.
    • 특징: 주문서에는 orderUp() 메소드(execute())가 포함되어 있으며, 이 메소드를 호출하면 주문이 실행됩니다. 주문서에는 식사를 준비할 주방장의 레퍼런스가 포함되어 있지만, 이를 사용하는 종업원은 그 세부 내용을 알 필요가 없습니다.
  • 종업원 (Invoker):
    • 역할: 종업원은 고객의 주문서를 받아서 이를 처리할 책임이 있습니다. 그러나 종업원은 주문의 세부 내용을 알지 못하며, 단지 orderUp() 메소드를 호출하여 주문이 실행되도록 합니다.
    • 특징: 종업원은 주문서를 전달하고 orderUp() 메소드를 호출하는 역할만 수행하며, 주문서에 무엇이 포함되어 있는지, 누가 음식을 준비하는지에 대해서는 알 필요가 없습니다. 이로써 종업원과 주방장은 완전히 분리됩니다.
  • 주방장 (Receiver):
    • 역할: 주방장은 실제로 주문된 음식을 준비하는 역할을 합니다. 주문서에 포함된 정보를 사용하여 음식을 만들며, 이 과정은 주방장만이 알고 있습니다.
    • 특징: 주방장은 종업원과 직접 상호작용하지 않습니다. 종업원이 orderUp() 메소드를 호출하면, 주방장은 그에 따라 음식을 준비하는 작업을 처리합니다.

 

2. 객체마을 식당과 커맨드 패턴

3. 첫번째 커맨드 객체 만들기

예시로 여러 가지 기능을 수행할 수 있는 리모컨을 들어보겠습니다.

 

1. 커맨드 인터페이스 구현

커맨드 객체는 모두 같은 인터페이스를 구현해야 합니다. 인터페이스에는 execute() 메소드 하나만 존재합니다. (이름은 변경 가능 ex. orderUp())

public interface Command {
    public void execute();
}

 

2. 조명을 켤 때 필요한 커맨드 클래스 구현

Light는 Receiver

public class Light {
    
    // 조명을 켜는 메소드
    public void on() {
        System.out.println("The light is on");
    }

    // 조명을 끄는 메소드
    public void off() {
        System.out.println("The light is off");
    }
}
public class LightOnCommand implements Command {
    Light light;

    // 생성자: 제어할 Light 객체를 받아서 저장
    public LightOnCommand(Light light) {
        this.light = light;
    }

    // Command 인터페이스의 execute() 메소드 구현
    public void execute() {
        light.on();  // Light 객체의 on() 메소드를 호출하여 조명을 켬
    }
}

 

  • LightOnCommand 클래스: 조명을 켜는 커맨드 객체입니다.
  • Light 객체: 커맨드 객체가 제어할 조명을 나타냅니다.
  • 생성자: Light 객체를 받아서 내부에 저장합니다.
  • execute() 메소드: 저장된 Light 객체의 on() 메소드를 호출하여 조명을 켭니다.

 

3. 커맨드 객체 사용하기

SimpleRemotecontrol은 Invoker

public class SimpleRemoteControl {
    Command slot;  // 커맨드를 저장할 슬롯

    public SimpleRemoteControl() {
        // 기본 생성자
    }

    // 커맨드를 설정하는 메소드
    public void setCommand(Command command) {
        slot = command;
    }

    // 버튼이 눌렸을 때 커맨드를 실행하는 메소드
    public void buttonWasPressed() {
        slot.execute();  // 슬롯에 저장된 커맨드를 실행
    }
}

 

  • SimpleRemoteControl 클래스: 매우 간단한 리모컨을 나타내며, 하나의 커맨드 객체만을 저장하고 실행할 수 있습니다.
  • slot 변수: Command 인터페이스를 구현한 객체를 저장하는 변수입니다.
  • setCommand() 메소드: 리모컨의 버튼에 연결될 커맨드를 설정합니다.
  • buttonWasPressed() 메소드: 버튼이 눌렸을 때 호출되며, 슬롯에 저장된 커맨드 객체의 execute() 메소드를 호출합니다.
public class RemoteControlTest {

    public static void main(String[] args) {
        // SimpleRemoteControl 인스턴스 생성 (Invoker 역할) (종업원)
        SimpleRemoteControl remote = new SimpleRemoteControl();

        // Light 인스턴스 생성 (Receiver 역할) (주방장)
        Light light = new Light();

        // LightOnCommand 인스턴스 생성 (Command 역할), light 객체를 전달
        LightOnCommand lightOn = new LightOnCommand(light);

        // 리모컨에 커맨드 설정
        remote.setCommand(lightOn);

        // 버튼을 눌러 커맨드 실행 (조명을 켜는 작업 수행)
        remote.buttonWasPressed();
    }
}

 

  • Invoker 역할 (SimpleRemoteControl 클래스):
    • SimpleRemoteControl 클래스의 remote 객체가 Invoker 역할을 합니다. 이 객체는 커맨드를 저장하고, 버튼이 눌렸을 때 해당 커맨드를 실행하는 역할을 합니다.
  • Receiver 역할 (Light 클래스):
    • Light 클래스의 light 객체는 Receiver 역할을 합니다. 실제로 조명을 켜고 끄는 작업을 수행하는 객체입니다.
  • Command 역할 (LightOnCommand 클래스):
    • LightOnCommand 클래스는 Command 인터페이스를 구현하여 조명을 켜는 작업을 캡슐화한 커맨드 객체입니다. light 객체를 생성자에서 받아서 execute() 메소드가 호출되면 light.on()을 실행합니다.
  • 커맨드 실행:
    • remote.setCommand(lightOn)을 통해 리모컨의 버튼에 LightOnCommand 커맨드를 설정하고, remote.buttonWasPressed() 메소드를 호출하면 커맨드가 실행되어 조명이 켜집니다.

4. 커맨트 패턴의 정의 & 다이어그램

커맨드 패턴(Command Pattern)을 사용하면 요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화(command 객체를 넘김)할 수 있습니다. (ex. remote.setCommand(lightOn))

이러면 요청을 큐에 저장하거나 로그로 기록하거나 작업 취소 기능을 사용할 수 있습니다.

 

 

5. 리모컨 코드 만들기

public class RemoteControl {
    Command[] onCommands;
    Command[] offCommands;

    public RemoteControl() {
        onCommands = new Command[7];
        offCommands = new Command[7];
        Command noCommand = new NoCommand();

        // 모든 슬롯에 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;
    }

    // on 버튼을 누르면 해당 슬롯의 onCommand 실행
    public void onButtonWasPushed(int slot) {
        onCommands[slot].execute();
    }

    // off 버튼을 누르면 해당 슬롯의 offCommand 실행
    public void offButtonWasPushed(int slot) {
        offCommands[slot].execute();
    }

    // 리모컨의 상태를 출력
    public String toString() {
        StringBuffer stringBuff = new StringBuffer();
        stringBuff.append("\n----- 리모컨 ---------\n");
        for (int i = 0; i < onCommands.length; i++) {
            stringBuff.append("[slot " + i + "] " + onCommands[i].getClass().getName()
                              + " " + offCommands[i].getClass().getName() + "\n");
        }
        return stringBuff.toString();
    }
}

 

커맨드 클래스 만들기

1. 조명 끄는 커맨트

public class LightOffCommand implements Command {
    Light light;

    // 생성자: 리시버인 Light 객체를 받아서 저장
    public LightOffCommand(Light light) {
        this.light = light;
    }

    // execute 메소드: 리시버의 off 메소드를 호출하여 조명을 끔
    public void execute() {
        light.off();
    }
}

 

2. 오디오 켜는 커맨드

오디오를 자동으로 CD 재생

public class StereoOnWithCDCommand implements Command {
    Stereo stereo;

    // 생성자: 제어할 Stereo 객체를 받아서 저장
    public StereoOnWithCDCommand(Stereo stereo) {
        this.stereo = stereo;
    }

    // execute 메소드: Stereo 객체의 on(), setCD(), setVolume() 메소드 호출
    public void execute() {
        stereo.on();        // 오디오 시스템을 켬
        stereo.setCD();     // CD 모드로 설정
        stereo.setVolume(11);  // 볼륨을 11로 설정
    }
}

 

리모컨 테스트

public class RemoteLoader {
    public static void main(String[] args) {
        // 리모컨 객체 생성
        RemoteControl remoteControl = new RemoteControl();

        // 각 장치 생성 (Receiver 들)
        Light livingRoomLight = new Light("Living Room");
        Light kitchenLight = new Light("Kitchen");
        CeilingFan ceilingFan = new CeilingFan("Living Room");
        GarageDoor garageDoor = new GarageDoor("Garage");
        Stereo stereo = new Stereo("Living Room");

        // 조명용 커맨드 객체 생성
        LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
        LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
        LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight);
        LightOffCommand kitchenLightOff = new LightOffCommand(kitchenLight);

        // 선풍기용 커맨드 객체 생성
        CeilingFanOnCommand ceilingFanOn = new CeilingFanOnCommand(ceilingFan);
        CeilingFanOffCommand ceilingFanOff = new CeilingFanOffCommand(ceilingFan);

        // 차고 문용 커맨드 객체 생성
        GarageDoorUpCommand garageDoorUp = new GarageDoorUpCommand(garageDoor);
        GarageDoorDownCommand garageDoorDown = new GarageDoorDownCommand(garageDoor);

        // 오디오 시스템용 커맨드 객체 생성
        StereoOnWithCDCommand stereoOnWithCD = new StereoOnWithCDCommand(stereo);
        StereoOffCommand stereoOff = new StereoOffCommand(stereo);

        // 리모컨 슬롯에 커맨드 로드
        remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
        remoteControl.setCommand(1, kitchenLightOn, kitchenLightOff);
        remoteControl.setCommand(2, ceilingFanOn, ceilingFanOff);
        remoteControl.setCommand(3, stereoOnWithCD, stereoOff);

        // 리모컨 버튼 테스트
        remoteControl.onButtonWasPushed(0);
        remoteControl.offButtonWasPushed(0);
        remoteControl.onButtonWasPushed(1);
        remoteControl.offButtonWasPushed(1);
        remoteControl.onButtonWasPushed(2);
        remoteControl.offButtonWasPushed(2);
        remoteControl.onButtonWasPushed(3);
        remoteControl.offButtonWasPushed(3);

        // 리모컨의 슬롯 정보를 출력
        System.out.println(remoteControl);
    }
}

6. 작업 취소 기능 추가하기

작업 취소 기능을 지원하려면 excute()와 비슷한 undo() 메소드가 있어야 합니다.

public interface Command {
    public void execute();  // 커맨드를 실행하는 메소드
    public void undo();     // 커맨드를 취소(되돌리기)하는 메소드
}

 

LightOnCommand의 undo 예시

public class LightOnCommand implements Command {
    Light light;

    // 생성자: 제어할 Light 객체를 받아서 저장
    public LightOnCommand(Light light) {
        this.light = light;
    }

    // execute 메소드: Light 객체의 on 메소드를 호출하여 조명을 켬
    public void execute() {
        light.on();
    }

    // undo 메소드: Light 객체의 off 메소드를 호출하여 조명을 끔 (실행 취소)
    public void undo() {
        light.off();
    }
}

 

RemoteControlWithUndo

public class RemoteControlWithUndo {
    Command[] onCommands;
    Command[] offCommands;
    Command undoCommand;  // 마지막으로 실행된 커맨드를 저장하는 변수

    public RemoteControlWithUndo() {
        onCommands = new Command[7];
        offCommands = new Command[7];
        
        Command noCommand = new NoCommand();
        for (int i = 0; i < 7; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
        undoCommand = noCommand;  // 초기값으로 noCommand 설정
    }

    // 특정 슬롯에 onCommand와 offCommand 설정
    public void setCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }

    // on 버튼이 눌렸을 때 실행
    public void onButtonWasPushed(int slot) {
        onCommands[slot].execute();  // 커맨드 실행
        undoCommand = onCommands[slot];  // 마지막으로 실행된 커맨드를 undoCommand에 저장
    }

    // off 버튼이 눌렸을 때 실행
    public void offButtonWasPushed(int slot) {
        offCommands[slot].execute();  // 커맨드 실행
        undoCommand = offCommands[slot];  // 마지막으로 실행된 커맨드를 undoCommand에 저장
    }

    // undo 버튼이 눌렸을 때 실행
    public void undoButtonWasPushed() {
        undoCommand.undo();  // 마지막으로 실행된 커맨드의 undo 메소드 호출
    }

    // 리모컨의 상태를 출력하는 toString 메소드 (생략 가능)
    public String toString() {
        StringBuilder stringBuff = new StringBuilder();
        stringBuff.append("\n----- 리모컨 ---------\n");
        for (int i = 0; i < onCommands.length; i++) {
            stringBuff.append("[slot " + i + "] " + onCommands[i].getClass().getName()
                              + "    " + offCommands[i].getClass().getName() + "\n");
        }
        stringBuff.append("[undo] " + undoCommand.getClass().getName() + "\n");
        return stringBuff.toString();
    }
}

 

RemoteLoader에서 undo 실행

public class RemoteLoader {
    public static void main(String[] args) {
        // Undo 기능이 있는 리모컨 객체 생성
        RemoteControlWithUndo remoteControl = new RemoteControlWithUndo();

        // 거실 조명 객체 생성
        Light livingRoomLight = new Light("Living Room");

        // 조명을 켜고 끄는 커맨드 객체 생성
        LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
        LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);

        // 리모컨의 0번 슬롯에 조명 커맨드 설정
        remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);

        // 조명을 켜고 끄는 명령을 실행
        remoteControl.onButtonWasPushed(0);  // 조명을 켬
        remoteControl.offButtonWasPushed(0); // 조명을 끔

        // 현재 리모컨의 상태 출력
        System.out.println(remoteControl);

        // 마지막 명령을 취소 (undo)
        remoteControl.undoButtonWasPushed(); // 조명을 다시 켬 (undo)

        // 다시 조명을 끄고, 켜는 명령을 실행
        remoteControl.offButtonWasPushed(0); // 조명을 끔
        remoteControl.onButtonWasPushed(0);  // 조명을 켬

        // 현재 리모컨의 상태 출력
        System.out.println(remoteControl);

        // 마지막 명령을 취소 (undo)
        remoteControl.undoButtonWasPushed(); // 조명을 다시 끔 (undo)
    }
}

7. 여러 동작을 한 번에 처리하기

매크로 커맨드

public class MacroCommand implements Command {
    Command[] commands;  // 여러 커맨드를 담을 배열

    // 생성자: Command 배열을 받아서 MacroCommand 안에 저장
    public MacroCommand(Command[] commands) {
        this.commands = commands;
    }

    // execute 메소드: 배열에 저장된 모든 커맨드를 실행
    public void execute() {
        for (int i = 0; i < commands.length; i++) {
            commands[i].execute();
        }
    }

    // undo 메소드: 배열에 저장된 모든 커맨드를 거꾸로 실행 취소
    public void undo() {
        for (int i = commands.length - 1; i >= 0; i--) {
            commands[i].undo();
        }
    }
}

 

매크로 커맨드 사용하기

public class RemoteLoader {
    public static void main(String[] args) {
        RemoteControlWithUndo remoteControl = new RemoteControlWithUndo();

        // 장치들 생성
        Light livingRoomLight = new Light("Living Room");
        Stereo stereo = new Stereo("Living Room");
        CeilingFan ceilingFan = new CeilingFan("Living Room");

        // 개별 커맨드 생성
        LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
        StereoOnWithCDCommand stereoOnWithCD = new StereoOnWithCDCommand(stereo);
        CeilingFanOnCommand ceilingFanOn = new CeilingFanOnCommand(ceilingFan);

        LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
        StereoOffCommand stereoOff = new StereoOffCommand(stereo);
        CeilingFanOffCommand ceilingFanOff = new CeilingFanOffCommand(ceilingFan);

        // MacroCommand로 여러 커맨드를 묶음
        Command[] partyOn = { livingRoomLightOn, stereoOnWithCD, ceilingFanOn };
        Command[] partyOff = { livingRoomLightOff, stereoOff, ceilingFanOff };

        MacroCommand partyOnMacro = new MacroCommand(partyOn);
        MacroCommand partyOffMacro = new MacroCommand(partyOff);

        // 리모컨에 MacroCommand 설정
        remoteControl.setCommand(0, partyOnMacro, partyOffMacro);

        // 리모컨 테스트
        System.out.println("--- Pushing Macro On---");
        remoteControl.onButtonWasPushed(0);  // 여러 장치가 한 번에 켜짐

        System.out.println("--- Pushing Macro Off---");
        remoteControl.offButtonWasPushed(0);  // 여러 장치가 한 번에 꺼짐

        System.out.println("--- Pushing Undo---");
        remoteControl.undoButtonWasPushed();  // 마지막 명령들을 모두 취소
    }
}

 

Comments