커맨드 패턴의 정의

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

커맨드 패턴의 목적

  • 한 차원 높은 단계의 캡슐화. 즉 메소드 호출을 캡슐화하는 것. 메소드 호출을 캡슐화하면 계산 과정의 각 부분들을 결정화시킬 수 있기 때문에, 계산하는 코드를 호출한 객체에서는 어떤 식으로 일을 처리해야 하는지에 대해 전혀 신경쓰지 않아도 된다. 그냥 결정화된 메소드를 호출해서 필요한 일만 잘 할 수 있으면 된다. 그 외에도 캡슐화된 메소드 호출을 로그 기록용으로 저장을 한다거나 취소(undo)기능을 구현하기 위해 재사용하는 것과 같은 작업도 할 수 있다.

요구사항

  • 리모컨 API를 만들어야 한다.
  • 업체에서 공급 받은 자바 클래스들이 있다.

클래스들을 살펴보자

TV
on()
off()
setInputChannel()
setVolume()
Light
on()
off()
FaucetControl
openValue()
closeValue()

클래스 분석

  • 공통적인 인터페이스가 있지는 않은거 같음.
  • 가장 큰 문제는 앞으로도 이런 클래스들이 더 추가될 수가 있다는 것

디자인 설계에 앞서…

  • 모든 클래스에 on()하고 off()메소드만 있을 줄 알았는데 다른 메소들이 잔뜩 있다.
  • 다른 제품이 추가 될 경우 다른 이름을 가진 메소드들이 추가될 수 있다.
  • 리모컨 버튼을 클릭하면 자동으로 일을 처리하도록 만들어야 된다.
  • 리모컨 자체에서는 티비의 볼륨을 높인다던지 하는 기능에 대한 자세한 내용은 모르도록 해야한다.
  • 리모컨 제품 제작 업체가 제공한 클래스에 대해 자세하게 알 필요가 없도록 만들어야 된다.
  • 커맨드 패턴이라는 걸 쓰면 어떤 작업을 요청하는 쪽하고 그 작업을 처리한 쪽을 분리시킬 수 있다고 한다.
  • 리모컨에서 작업을 요청하고 업체에서 공급한 클래스에서 그 작업을 처리한다고 보면 커맨드 패턴을 적용할 수 있을 거 같다.

커맨드 패턴의 소개에 앞서

  • 식당의 운영 방식 소개
  1. 고객이 웨이트리스한테 주문
  2. 웨이트리스는 주문을 받아 카운터에 가져다 줌
  3. 주방장이 주문대로 음식을 준비
  • 이것을 객체와 메소드를 이용하여 적어보자
  1. createOrder() 고객이 주문한 내용생성
  2. takeOrder() 그 내용을 웨이트리스한테 전달
  3. orderUp() 주문을 처리하기 위해 준비단계
  4. makeBurger(), makeShake() 객체가 주방장에서 요리 생성 메소드를 호출하여 지시를 내림
  • 객체와 메소드간의 역할을 알아보자
  1. 주문서는 주문한 메뉴를 캡슐화한다. (웨이트리스는 주문서에 주문화 된 내용이 캡슐화 되어있기에 내용을 몰라도 된다. 그저 적당한 곳에 주문서를 전달만 하면 된다.)
  2. 웨이트리리스는 주문서를 받아서 거기에 있는 orderUp()메소드를 호출하는 일을 한다. (여러 고객들은 takeOrder() 메소드에서 여러 고객이 여러 주문서를 매개변수로 전달합니다. 하지만 주문이 상당히 많아도 별로 어려운 것은 없다. 모든 주문서는 orderUp()이 있고 이 메소드만 호출하면 식사가 준비가 된다.)
  3. 주방장은 식사를 준비하는데 필요한 정보를 가지고 있다. (중요한 점은 실제로 식사를 준비하는 방법은 주방장만 알고 있다는 것이다. 웨이트리스는 각 주문서에 있는 메소드를 호출 할 뿐이고, 주방장은 주문서로부터 할 일을 전달 받는다. 즉 웨이트리스하고 직접 얘기를 할 필요가 전혀 없다는 것이다.
  • 정리

식당은 어떤 것을 요구하는 객체와 그 요구를 받아들이고 처리하는 객체를 분리시키는 객체지향 디자인 패턴의 한 모델이다. 이것을 위의 리모컨 API와 비교해 보면 리모컨 버튼을 눌렸을 때 호출되는 코드와 특정 업체에서 제공한, 실제로 일을 처리하는 코드를 분리시키는 것이 가능하다. 버튼을 눌렸을 때 그 객체의 “orderUp()”같은 메소드가 호출되면서, 리모컨에서는 어떤 객체가 무슨일을 하는지 모르지만 불이 켜진다는 식으로 일이 처리가 가능할 것이다.

식당과 커맨드패턴의 비교

  • 식당과 커맨드 패턴을 비교해보자

클라이언트 -> createCommandObject() -> 커맨드(execute()) -> 인보커(setCommand()) -> execute() -> 리시버(action1, action2)

  • 클라이언트는 커맨드 객체를 생성해야 한다. 커맨드 객체는 리시버에 전달할 일련의 행동으로 구성된다.
  • 커맨드 객체에는 행동과 리시버에 대한 정보가 같이 들어있다.
  • 커맨드 객체에서 제공하는 메소드는 execute() 하나 뿐이다. 이 메소드는 행동을 캡슐화하고 리시버에 있는 특정 행동을 처리한다.
  • 클라이언트에서는 인보커 객체의 setCommand() 메소드를 호출하는데, 이때 커맨드 객체를 넘겨준다. 그 커맨드 객체는 나중에 쓰이기 전까지 인보커 객체에 보관된다.
  • 인보커에서 커맨드 객체의 execute()를 호출하면 리시버에 있는 특정 행동을 하는 메소드가 호출 됩니다.

  • 식당과 커맨드 패턴의 비교
  1. 웨이트리스 - 커맨드객체
  2. 주방장 - execute()
  3. orderUp - 클라이언트 객체
  4. 주문서 - 인보커 객체
  5. 손님 - 리시버 객체
  6. takeOrder() - setCommand()

첫 번째 커맨드 객체

  • Command 인터페이스 만들기
public interface Command {
	public void execute();
}
  • 전등을 켜기 위한 인터페이스 구현
    public class LightOnCommand implements Command {
      Light light;
    
      public LightOnCommand(Light light) {
          this.light = light;
      }
    
      @Override
      public void execute() {
          light.on();
      }
    }
    
public class Light{
	public void on() {
		System.out.println("light is on");
	}
}
  • 커맨드 객체 사용하기
public class SimpleRemoteControl {
	Command slot;

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

	public void buttonWasPressed() {
		slot.execute();
	}
}
  • 리모컨을 사용하기 위한 간단한 테스트 클래스
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.buttonWasPressed();

	}

}

차고문을 열기위한 GarageDoorOpenCommand클래스를 만들어보자

  • GarageDoor 클래스
TV
up()
down()
stop()
lightOn()
lightOff()
public class GarageDoor {

	public void up() {
		System.out.println("Garage Door is open");
	}

	public void down() {
		System.out.println("Garage Door is close");
	}

	public void stop() {
		System.out.println("Garage Door is stop");
	}

	public void lightOn() {
		System.out.println("Garage Door is light on");
	}

	public void lightOff() {
		System.out.println("Garage Door is light off");
	}
}
public class GarageDoorOpenCommand implements Command {

	GarageDoor garageDoor;


	public GarageDoorOpenCommand(GarageDoor garageDoor) {
		this.garageDoor = garageDoor;
	}


	@Override
	public void execute() {
		garageDoor.up();
	}
}
public class RemoteControlTest {

	public static void main(String[] args) {
		SimpleRemoteControl remote = new SimpleRemoteControl();
		Light light = new Light();
		GarageDoor garageDoor = new GarageDoor();
		LightOnCommand lightOn = new LightOnCommand(light);
		GarageDoorOpenCommand garageOpen = new GarageDoorOpenCommand(garageDoor);

		remote.setCommand(lightOn);
		remote.buttonWasPressed();
		remote.setCommand(garageOpen);
		remote.buttonWasPressed();

	}

}

커맨드 패턴의 정의

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

  • 커맨드 객체는 일련의 행동을 특정 리시버하고 연결시킴으로써 요구사항을 캡슐화한 것이다. 이렇게 하기 위해 행동과 리시버를 한 객체에 집어 넣고, execute()라는 메소드만 외부에 공개하는 방법을 사용한다. *외부에서 볼 때는 어떤 객체가 리시버 역할을 하는지, 리시버가 실제 어떤 일을 하는지 알 수 없다. *명령을 통해서 객체를 매개변수화하는 예도 볼수 있었다. 예를 들어 전등켜기를 했다가 차고 문 열기 명령을 로딩하기도 했다. *그리고 커맨드 패턴을 조금만 확장하면 메타 커맨드 패턴이라는 것도 사용이 가능한데 메타, 커맨드 패턴은 명령들로 이루어진 매크로를 만들어서 여러 개의 명령을 한 번에 실행할 수 있다.

리모컨 코드

package com.kws.command.remote;

import com.kws.command.Command;
import com.kws.command.NoCommand;

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

	public RemoteControl() {
		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;
		}
	}

	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 stringBuffer = new StringBuffer();
		stringBuffer.append("\n----- Remote Control ------ \n");

		for (int i = 0; i < onCommands.length; i++) {
			stringBuffer.append("[slot " + i + "]" + onCommands[i].getClass().getName() + "   " + offCommands.getClass().getName() + "\n");
		}

		return stringBuffer.toString();
	}
}
package com.kws.command;

import com.kws.command.command.GarageDoor;
import com.kws.command.command.GarageDoorCloseCommand;
import com.kws.command.command.GarageDoorOpenCommand;
import com.kws.command.command.Light;
import com.kws.command.command.LightOffCommand;
import com.kws.command.command.LightOnCommand;
import com.kws.command.command.Stereo;
import com.kws.command.command.StereoOffWithCDCommand;
import com.kws.command.command.StereoOnWithCDCommand;
import com.kws.command.remote.RemoteControl;

public class RemoteControlTest {

	public static void main(String[] args) {

		//기본테스트
//		SimpleRemoteControl remote = new SimpleRemoteControl();
//		Light light = new Light();
//		GarageDoor garageDoor = new GarageDoor();
//		LightOnCommand lightOn = new LightOnCommand(light);
//		GarageDoorOpenCommand garageOpen = new GarageDoorOpenCommand(garageDoor);
//		
//		remote.setCommand(lightOn);
//		remote.buttonWasPressed();
//		remote.setCommand(garageOpen);
//		remote.buttonWasPressed();


		RemoteControl remoteControl = new RemoteControl();

		Light livingRoomLight = new Light("Living Room");
		Light kitchenLight = new Light("kitchen Room");
		GarageDoor garageDoor = new GarageDoor("");
		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);

		GarageDoorOpenCommand garageDoorOpen = new GarageDoorOpenCommand(garageDoor);
		GarageDoorCloseCommand garageDoorClose = new GarageDoorCloseCommand(garageDoor);

		StereoOnWithCDCommand stereoOnWithCD = new StereoOnWithCDCommand(stereo);
		StereoOffWithCDCommand stereoOffWithCD = new StereoOffWithCDCommand(stereo);



		remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
		remoteControl.setCommand(1, kitchenLightOn, kitchenLightOff);
		remoteControl.setCommand(2, garageDoorOpen, garageDoorClose);
		remoteControl.setCommand(3, stereoOnWithCD, stereoOffWithCD);

		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);
	}

}

NoCommand???

*NoCommand 객체는 일종의 널 객체이다. 딱히 리턴할 객체는 없지만 클라이언트 쪽에서 null을 처리하지 않아도 되도록 하고 싶을 때 널 객체를 활용하면 좋다. 리모컨에서는 execute()메소드가 호출됐을 때 아무 일도 하지 않지만 빈 자리를 채우기 위한 용도로 NoCommand라는 객체를 집어 넣어 두었다. 널 객체는 여러 디자인 패턴에서 유용하게 쓰인다. 널 객체를 일종의 디자인 패턴으로 분류하기도 한다.

###커맨드 패턴 활용

  • 요청을 큐에 저장하기
  • 요청을 로그에 기록하기

핵심정리

  • 커맨드 패턴을 이용하면 요청을 하는 객체와 그 요청을 수행하는 객체를 분리시킬 수 있다.
  • 이렇게 분리시키는 과정의 중심에는 커맨드 객체가 있으며, 이 객체가 행동이 들어있는 리시버를 캡슐화 한다.
  • 인보커에서는 요청을 할 때는 커맨드 객체의 execute 메소드를 호출하면 된다.
  • 인보커는 커맨드를 통해서 매개변수화 될 수 있다. 실행중에 동적으로 설정할 수도 있다.
  • execute()메소드가 마지막으로 호출되기 전의 기존 상태로 되돌리기 위한 작업취소 메소드를 구현하면 커맨드 패턴을 통해서 작업취소 기능을 지원 할 수도 있다.
  • 프로그래밍을 하다 보면 요청 자체를 리시버한테 넘기지 않고 자기가 처리하는 커맨드 객체를 사용하는 경우도 종종 있다.
  • 커맨드 패턴을 활용하여 로그 및 트랜잭션 시스템을 구현하는 것도 가능하다.

커맨드 패턴의 단점

  • 객체 구성 부분이 추가 되면 추상부분부터 수정해서 추가해 줘야 한다.