읽은 책 정리/Head Firstr Design Pattern

CHAPTER 06.커멘드 패턴

커멘드 패턴 이해를 위한 간단한 문제 제안

  • 필자는 고객의 요구사항에 의해서 만능 IOT 리모컨을 만들려고 합니다.
  • 리모컨에는 아래와 같은 방식으로 만들려고 해요!
  • 그림을 보면 7개의 기능들이 각 슬롯마다 매핑되어야 하는것을 알 수 있습니다.
  • 문제는 각 기능들의 공통된 인터페이스가 없다라는 상황입니다.
  • 예를 들어 ON의 첫 번째 버튼은 TV의 관련 기능이고 두 번째는 선풍기 전원이라고 가정하겠습니다.
    • TV의 기능 ON에는 TV가 켜진순간 볼륨도 켜져야하고 채널의 최초 번호수도 결정되어야 합니다.
    • 선풍기는 ON 되는 순간 방향의 세기가 결정되어야 합니다.
  • 이처럼 공통 인터페이스가 없는 경우 위의 사진처럼 여러 클래스가 난잡해지는 문제가 발생합니다.
  • 하물며 고객은 나중에 다른 제품이 추가될 수 있게 확장성 있는 개발을 원합니다.
  • 이런 경우 문제를 해결하기 위해 등잔한 디자인 패턴이 커멘트 패턴입니다.

커맨드 패턴이란

  • 리모컨처럼 작업을 요청하는 요청자와 작업을 수행하는 대상을 분리하는것이 커맨트 패턴의 핵심 개념입니다.
  • 커맨드 패턴을 이해하기 쉽게 식당에서 주문을 하는 예시를 들어 커멘드 패턴을 소개해보겠습니다.
  • 먼저 고객이 주문을 시도합니다. 이때 주문(createOrder)이라는 행동이 수행되고 주문(Order)이 생성됩니다.
  • 이후 주문 처리(takeOrder())가 수행되고 종업원은 주문을 주방장에게 전달(OrderUp)하게 됩니다.
  • 주문서를 전달받은 주방장은 요리를 수행하게 되고(make()) 음식이라는 결과물이 나오게 됩니다.

식당 주문 절차를 객체와 메소드를 통해 연관 지어보기

  • 주문서는 종업원에게 전달해주는 방식으로 주문 내용을 캡슐화하고 있습니다. 주문서 객체에는 식사 준비하기 위한 행동인 orderUp() 메소드가 캡슐화 되어 있습니다. 또한 식사를 주문해야 하는 객체(주방장)의 참조도 들어있습니다.
  • 종업원은 주문서의 어떠한 정보도 모르며 어떤 주방장이 요리할지에 대한 정보도 모릅니다. 주방장에게 주문서 정보를 던져주기만 하면 되죠. 코드적인 측면으로는 캡슐화가 되었다는것을 의미합니다.
  • 종업원은 주문서를 받고 orderUp() 메소드를 호출합니다.
    • orderUp()메소드를 호출해서 주방장을 호출하기만 하면 됩니다.
    • 종업원의 takeOrder() 메소드에는 여러 고객의 주문서를 매개변수로 전달합니다.
  • 위의 절차대로 주방장은 식사를 준비하는데 필요한 정보를 가지고 있습니다.
  • 위의 절차대로 각 객체(고객,주문서,종업원,주방장)들은 서로에 대한 정보를 몰라도 됩니다.
    • 즉 커맨드 패턴은 어떤 것을 요구하는 객체와 그 요구를 받아 처리하는 객체를 분리한 디자인 패턴의 모델인것을 알 수 있습니다.

커맨드 패턴으로 살펴보는 다이어그램

  • 식당에서 주문을 하는 절차들을 커맨드 패턴으로 변화한 것입니다. 위의 정보들은 아래처럼 바뀌게 됩니다.
    • 고객 → 클라이언트 객체
    • 주문서 → 커멘드 객체
    • takeOrder() → setCommand()
    • 종업원 → 인보커 객체
    • orderUp() → execute()
    • 주방장 → 리시버 객체
  • 먼저 클라이언트가 커멘드 객체를 생성합니다.
    • 개발자가 커멘드 객체를 생성한다고 생각하면 됩니다.
  • 이후 커멘드 객체에 의존하고 있는 인보커 객체의 setCommand를 호출하여 커멘드 객체를 저장합니다,
  • 마지막으로 커멘드 객체에 의존하고 있는 인보커 객체를 커멘드 객체의 execute()를 통해 수행합니다.

커멘드 패턴 구현하기

  • 위의 개념을 학습하여 커맨드 패턴을 활용하여 리모컨의 조명 통제 기능을 추가해보도록 하겠습니다.

커멘드 인터페이스 구현

public interface Command {
	public void execute();
}
  • 커멘드 객체는 모두 같은 인터페이스를 구현해야하기 커멘트 객체는 때문에 위의 interface를 정의하여 사용해야 합니다.
  • 식당 다이어그램에서는 orderUp()이라는 예제를 사용했지만 범용적으로 execute()라는 이름을 많이 사용합니다.

커멘드 클래스 구현

public class LightOnCommand implements Command {

    Light light;

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

    public void execute() {
        light.on();
    }
}
  • 조명을 켜는 명령을 내리는 LightOnCommand입니다.
  • 생성자에 커멘드 객체를 통해 수행될 대상 객체를 전달해줍니다.
    • 이후 execute() 메소드를 통해 수행됩니다.

커멘드 객체 사용하기

//
// This is the invoker
//
public class SimpleRemoteControl {
	Command slot;
 
	public SimpleRemoteControl() {}
 
	public void setCommand(Command command) {
		slot = command;
	}
 
	public void buttonWasPressed() {
		slot.execute();
	}
}
  • 예제를 간단히 진행하기 위해 리모컨 버튼이 하나만 있다고 가정하겠습니다.
  • setCommand()를 통해 Command를 결합합니다.
    • 리모컨 버튼의 기능을 바꾸고 싶다면 해당 메소드만 변경하면 얼마든지 유동적으로 변경이 가능합니다.
  • buttionWasPressed()를 사용해 execute 메소드가 호출됩니다.

커멘드 객체 테스트하기


public class RemoteControlTest {
	public static void main(String[] args) { //클라이언트
		SimpleRemoteControl remote = new SimpleRemoteControl(); // Invoker 객체 생성
		Light light = new Light(); //Command 객체의 실행 로직을 가지고 있는 객체 생성
		LightOnCommand lightOn = new LightOnCommand(light); //Command 객체 생성
		
		remote.setCommand(lightOn);
		remote.buttonWasPressed();
    }
	
}

실행결과

기능이 여러개인 리모컨 만들기

@FunctionalInterface
public interface Command {
	public void execute();
}
  • 위와 마찬가지로 커멘드 객체의 의 기능을 공통화하기 위한 인터페이스입니다.
public class CeilingFan {
	String location = "";
	int level;
	public static final int HIGH = 2;
	public static final int MEDIUM = 1;
	public static final int LOW = 0;
 
	public CeilingFan(String location) {
		this.location = location;
	}
  
	public void high() {
		// turns the ceiling fan on to high
		level = HIGH;
		System.out.println(location + " ceiling fan is on high");
 
	} 

	public void medium() {
		// turns the ceiling fan on to medium
		level = MEDIUM;
		System.out.println(location + " ceiling fan is on medium");
	}

	public void low() {
		// turns the ceiling fan on to low
		level = LOW;
		System.out.println(location + " ceiling fan is on low");
	}
 
	public void off() {
		// turns the ceiling fan off
		level = 0;
		System.out.println(location + " ceiling fan is off");
	}
 
	public int getSpeed() {
		return level;
	}
}
public class Light {
	String location = "";

	public Light(String location) {
		this.location = location;
	}

	public void on() {
		System.out.println(location + " light is on");
	}

	public void off() {
		System.out.println(location + " light is off");
	}
}
public class GarageDoor {
	String location;

	public GarageDoor(String location) {
		this.location = location;
	}

	public void up() {
		System.out.println(location + " garage Door is Up");
	}

	public void down() {
		System.out.println(location + " garage Door is Down");
	}

	public void stop() {
		System.out.println(location + " garage Door is Stopped");
	}

	public void lightOn() {
		System.out.println(location + " garage light is on");
	}

	public void lightOff() {
		System.out.println(location + " garage light is off");
	}
}
public class Stereo {
	String location;

	public Stereo(String location) {
		this.location = location;
	}

	public void on() {
		System.out.println(location + " stereo is on");
	}

	public void off() {
		System.out.println(location + " stereo is off");
	}

	public void setCD() {
		System.out.println(location + " stereo is set for CD input");
	}

	public void setDVD() {
		System.out.println(location + " stereo is set for DVD input");
	}

	public void setRadio() {
		System.out.println(location + " stereo is set for Radio");
	}

	public void setVolume(int volume) {
		// code to set the volume
		// valid range: 1-11 (after all 11 is better than 10, right?)
		System.out.println(location + " stereo volume set to " + volume);
	}
}
  • CeilingFan, Light, GarageDoor, Stereo 모두 Invoker를 통해 수행될 대상이 되는 객체들입니다.
//
// This is the invoker
//
public class RemoteControl {
	Command[] onCommands;
	Command[] offCommands;
 
	public RemoteControl() {
		onCommands = new Command[7];
		offCommands = new Command[7];
 
		for (int i = 0; i < 7; i++) {
			onCommands[i] = () -> { }; //NoCommand 객체를 람다식으로 대체
			offCommands[i] = () -> { };
		}
	}
  
	public void setCommand(int slot, Command onCommand, Command offCommand) {
		onCommands[slot] = onCommand;
		offCommands[slot] = offCommand;
	}
 
	public void onButtonWasPushed(int slot) {
		onCommands[slot].execute();
	}
 
	public void offButtonWasPushed(int slot) {
		offCommands[slot].execute();
	}

	public String toString() {
		StringBuffer stringBuff = new StringBuffer();
		stringBuff.append("\n------ Remote Control -------\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();
	}

}
  • Invoker 객체입니다.
  • 특이한 점은 NoCommand 객체를 람다식으로 대체했다는것입니다.
    • NoCommand 객체는 일종의 NULL 객체입니다. 예시 기준으로 리모컨에 사용되지 않는 버튼에 NoCommand 객체를 정의해서 execute 메소드를 호출해도 문제가 되지 않도록 설정한 것입니다.
    • NoCommand 객체는 여러 대자인 패턴에서 유용하게 사용됩니다.
public class RemoteLoader {
 
	public static void main(String[] args) {
		RemoteControl remoteControl = new RemoteControl();
 
		Light livingRoomLight = new Light("Living Room");
		Light kitchenLight = new Light("Kitchen");
		CeilingFan ceilingFan= new CeilingFan("Living Room");
		GarageDoor garageDoor = new GarageDoor("Main house");
		Stereo stereo = new Stereo("Living Room");

		remoteControl.setCommand(0, livingRoomLight::on, livingRoomLight::off);
		remoteControl.setCommand(1, kitchenLight::on, kitchenLight::off);
		remoteControl.setCommand(2, ceilingFan::high, ceilingFan::off);
		
		Command stereoOnWithCD = () -> {
			stereo.on(); stereo.setCD(); stereo.setVolume(11);
		};
		remoteControl.setCommand(3, stereoOnWithCD, stereo::off);
		remoteControl.setCommand(4, garageDoor::up, garageDoor::down);
  
		System.out.println(remoteControl);
 
		remoteControl.onButtonWasPushed(0);
		remoteControl.offButtonWasPushed(0);
		remoteControl.onButtonWasPushed(1);
		remoteControl.offButtonWasPushed(1);
		remoteControl.onButtonWasPushed(2);
		remoteControl.offButtonWasPushed(2);
		remoteControl.onButtonWasPushed(3);
		remoteControl.offButtonWasPushed(3);
		remoteControl.onButtonWasPushed(4);  
		remoteControl.offButtonWasPushed(4);
		remoteControl.onButtonWasPushed(5);
	}
}
  • RemoteLoader에서의 테스트는 일반 테스트와 다를거 없지만 다른점이 있다면 setCommand에서 람다 형식으로 전달하기 때문에 커멘드 객체가 사라졌다는것입니다.
  • 사실상 람다식을 사용하면 커멘드 객체의 대상이 되는 객체가 커멘드 객체의 역할을 수행하게 됩니다.
    • ex:) Light, Stereo
  • 이러한 방법은 Command 인터페이스에 추상 메소드가 하나뿐일 때만 사용할 수 있습니다.
  • 이외에도 매크로를 설정하여 각 기능들을 한 번에 동작하기, undo(이전으로 되돌리기) 등 추가적이 구현이 가능합니다.

정리

  • 커맨트 패턴을 사용하면 요청하는 객체와 요청을 수행하는 객체를 분리할 수 있습니다.
  • 분리하는 과정에서 커멘드 객체가 있으며 커멘드 객체를 수행하는 Invoker가 존재합니다. Command 개체가 수행하는 행동을 캡슐화한것이 리시버입니다.
  • 여러개의 커멘드를 실행하는 매크로 커멘드를 구현할 수 도 있고 이전 작업으로 되돌리는 작업 되돌리기 기능을 만들수도 있다.
  • 커맨드 패턴은 요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화 할 수 있습니다.
  • 이러한 요청을 큐에 저장하거나 로그로 기록하는 용도로 사용할 수 있습니다.
    • 이러한 기록들은 서버에 장애가 발생하였을 때 최근 작업을 다시 시작할 수 있는 방향으로 사용할 수 도 있습니다.

ps. 266페이지의 핵심정리에서 “커멘드는 인보커를 매개변수” → “인보커는 커멘드를 매개변수”가 맞지 않나?