매서드 호출을 캡슐화한다! Command Pattern!
요청 내역을 객체로 캡슐화해서, 객체를 서로 다른 요청 내역에 따라 매개변수화 할 수 있다. 이로 인해서 요청을 Queue에 저장하거나, 로그로 기록하거나 "요청"에 대한 작업 취소(롤백) 기능을 사용할 수 있다.
어떻게 이렇게 되는가? -> 실행될 기능의 변경에도 호출자 클래스를 수정 없이 그대로 사용 할 수 있도록 해주기 때문
의존관계 역전 원칙, (DIP) 는 다음과 같이 정의됩니다.
"상위 모듈은 하위 모듈의 구현 내용에 의존하면 안 된다. 상위 모듈과 하위 모듈 모두 추상화된 내용에 의존해야 한다."
차세대 홈 오토메이션 장비를 제어할 리모콘을 만들어 달라는 요청을 받았다. 이번에도 거절하기엔 너무 큰 액수를 제안받은 우리, 리모콘을 깔끔 야물딱지게 만들어봅시다!
조명을 on/off 해야합니다
setTemperature() 매서드를 통해서 온도를 조절할 수 있어야 합니다
on, off, setCd, setDvd ... 여러 매서드가 존재합니다.
on/off 기반의 녀석들과, 또 다른 매서드를 가지는 여러 녀석들이 존재합니다.
이 행동들이 공통된 추상을 가지면 좋겠습니다. 그런데 문제는, 이 녀석들이 가지고 있는 매서드들이 전부 다 다르다는 것입니다.
이렇게 만들면, 새로운 클래스가 추가될 때마다 리모컨에 있는 코드를 고쳐야 하므로 좋지 않습니다.
DIP에 위배되는군요!
커맨드 패턴이 뭘까요?
식당에서 음식을 주문하고 있습니다.
다음과 같은 플로우로 진행될 듯 합니다.
음식 주문 : createOrder()
종업원이 음식 받기 : takeOrder()
주문서 : orderUp()
실제 음식을 만드는 부분 : makeFood()
위와 같이 캡슐화 한다면, 어떤 음식이 들어오던간에 주문서 부분에 호출만 적절히 해주면 클라이언트가 어떤 요청을 하던간에 상고나 없을 듯 합니다.
그렇다면, 이런 캡슐화가 코드단에서는 어떤 모습을 가질까요?
클라이언트는 커맨드 객체를 생성해야 합니다.
이 커맨드 객체는 리시버에 전달할 일련의 행동으로 구성됩니다!
public interface Command {
void execute();
}
이 매서드는 행동을 "캡슐화" 하며, 리시버에 있는 특정 행동읅 처리합니다.
public class TurnOnCommand implements Command{
private final Light light;
public TurnOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
}
커맨드 객체의 실제 구현체입니다.
execute(), receiver의 정보가 같이 들어있습니다.
public class Invoker {
private Command command;
public void setCommand(Command command) {
// 생성자에서 Command 를 특정, 이 생성자에는 Receiver 를 특정해야 합니다.
this.command = command;
}
public void executeCommand() {
command.execute();
}
}
public class Light {
public void turnOn() {
System.out.println("불을 켭니다.");
}
public void turnOff() {
System.out.println("불을 끕니다.");
}
}
클라이언트에서 커맨드 객체 생성
setCommand() 호출해서 인보커에 커맨드 객체를 저장
public void setCommand(Command command) {
// 생성자에서 Command 를 특정, 이 생성자에는 Receiver 를 특정해야 합니다.
this.command = command;
}
위와 같이 테스트 가능합니다.
public class Stereo {
int volume;
public void on(){
System.out.println("스테레오를 켭니다");
}
public void setCD(){
System.out.println("CD를 설정합니다.");
}
public void setVolume(int volume){
this.volume = volume;
System.out.println("볼륨이 " + volume + "으로 설정되었습니다.");
}
}
public class StereoOnWithCDCommand implements Command{
private final Stereo stereo;
public StereoOnWithCDCommand(Stereo stereo) {
this.stereo = stereo;
}
@Override
public void execute() {
stereo.on();
stereo.setCD();
stereo.setVolume(11); // 여기서는 그냥 맞춰줬습니다.
}
}
public class StereoOffCommand implements Command{
private final Stereo stereo;
public StereoOffCommand(Stereo stereo) {
this.stereo = stereo;
}
public void stereoOff(){
System.out.println("스테레오를 끕니다.");
}
@Override
public void execute() {
stereoOff();
}
}
public class Invoker {
private Command[] onCommands;
private Command[] offCommands;
public Invoker() {
onCommands = new Command[7]; // 버튼은 7개라고 가정합니다
offCommands = new Command[7];
Command noCommand = new NoCommand();
for (int i = 0; i < 7; i++) {
// 생성자이므로, 커맨드는 noCommand 로 초기화 합니다. (할당되지 않은 커맨드)
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
}
public Invoker(Command[] onCommands, Command[] offCommands) {
this.onCommands = onCommands;
this.offCommands = offCommands;
}
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();
}
}
인보커 클래스입니다.
Command[]을 통해서, onCommand 와 offCommand 를 각각 배열로 정의해서 별개의 슬롯에 꽂아줄 생각입니다.
리모컨에 실제 버튼을 할당한다고 생각하면 편합니다.
setCommand() 매서드가 변했습니다. 이제 슬롯을 할당해서, command[] 배열의 몇번 칸에 구현체 커맨들를 넣을지 결정 할 수 있습니다.
public Invoker() {
onCommands = new Command[7]; // 버튼은 7개라고 가정합니다
offCommands = new Command[7];
Command noCommand = new NoCommand();
for (int i = 0; i < 7; i++) {
// 생성자이므로, 커맨드는 noCommand 로 초기화 합니다. (할당되지 않은 커맨드)
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
}
따라서, "아직 할당되지 않은 버튼"의 커맨드를 정의해야 하니, NoCommand 도 정리했습니다.
위 코드 부분, 즉 생성자 부분에서의 초기화를 위해서입니다.
public class NoCommand implements Command{
@Override
public void execute() {
}
}
public class InvokerAsRemoteController {
public static void main(String[] args) {
Invoker invoker = new Invoker();
Light light = new Light();
LightOnCommand turnLightOn = new LightOnCommand(light);
LightOffCommand turnLightOff = new LightOffCommand(light);
Stereo stereo = new Stereo();
StereoOnWithCDCommand stereoOnWithCD = new StereoOnWithCDCommand(stereo);
StereoOffCommand stereoOff = new StereoOffCommand(stereo);
invoker.setCommand(1, turnLightOn, turnLightOff); // 1번 슬롯을 라이트 온오프로 설정
invoker.setCommand(2, stereoOnWithCD, stereoOff); // 2번 슬롯을 스테레오 온오프로 설정
invoker.onButtonWasPushed(1);
invoker.offButtonWasPushed(1);
}
}
한번 해보죠!
public interface Command {
void execute();
void undo();
}
public class LightOnCommand implements Command{
private final Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
@Override
public void undo() {
light.turnOff(); // on 의 undo 는 turnOff 이므로!
}
}
public class Invoker {
...
private Command undoCommand;
public Invoker() {
...
undoCommand = noCommand; // noCommand 로 초기화 해줍니다.
}
...
public void onButtonWasPushed(int slot){
onCommands[slot].execute();
undoCommand = onCommands[slot]; // undoCommand 에 마지막으로 누른 녀석을 할당합니다.
}
public void offButtonWasPushed(int slot){
offCommands[slot].execute();
undoCommand = offCommands[slot]; // undoCommand 에 마지막으로 누른 녀석을 할당합니다.
}
public void undoButtonWasPushed(){
undoCommand.undo();
}
}
생성자는 마찬가지로 noCommand 로 초기화합니다 (사용자가 아무것도 하지 않고 언두만 누르면 noCommand가 되게 합니다)
버튼이 눌렸을 때, execute() 매서드를 호출 한 후 그 객체의 레퍼런스를 undoCommand 변수에 할당합니다.
이를 통해서 마지막으로 실행된 기능이 undo에 저장되므로, undoButtonWasPushed() 매서드를 통해서 작업 취소가 가능합니다.
public class Invoker {
private Command[] onCommands;
private Command[] offCommands;
private Command undoCommand;
public Invoker() {
onCommands = new Command[7]; // 버튼은 7개라고 가정합니다
offCommands = new Command[7];
Command noCommand = new NoCommand();
for (int i = 0; i < 7; i++) {
// 생성자이므로, 커맨드는 noCommand 로 초기화 합니다. (할당되지 않은 커맨드)
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
undoCommand = noCommand; // noCommand 로 초기화 해줍니다.
}
public Invoker(Command[] onCommands, Command[] offCommands) {
this.onCommands = onCommands;
this.offCommands = offCommands;
}
public void setCommand(int slot, Command onCommand, Command offCommand){
onCommands[slot] = onCommand; // 해당 슬롯에
offCommands[slot] = offCommand; // 온커맨드, 오프커맨드를 할당합니다.
}
public void onButtonWasPushed(int slot){
onCommands[slot].execute();
undoCommand = onCommands[slot]; // undoCommand 에 마지막으로 누른 녀석을 할당합니다.
}
public void offButtonWasPushed(int slot){
offCommands[slot].execute();
undoCommand = onCommands[slot]; // undoCommand 에 마지막으로 누른 녀석을 할당합니다.
}
public void undoButtonWasPushed(){
undoCommand.undo();
}
}
당장 해봅시다
public class Fan { // 선풍기!
public static final int HIGH = 3;
public static final int MEDIUM = 2;
public static final int LOW = 1;
public static final int OFF = 0;
int speed;
public Fan() {
this.speed = OFF; // 초기에는 꺼진 상태로 시작합시다
}
public void setHigh(){
this.speed = HIGH;
}
public void setMedium(){
this.speed = MEDIUM;
}
public void setLow(){
this.speed = LOW;
}
public void setOff(){
this.speed = OFF;
}
public int getSpeed(){
return this.speed;
}
}
public class FanHighCommand implements Command{
private Fan fan;
int prevSpeed; // 상태를 저장해줄 생각입니다.
public FanHighCommand(Fan fan) {
this.fan = fan;
}
@Override
public void execute() {
prevSpeed = fan.getSpeed(); // 팬 객체에 에 이미 저장되어있는, 이전 스피드를 가져옵니다.
fan.setHigh();
System.out.println("High 커맨드 선택되었습니다.");
System.out.println("이전 스피드는 " + prevSpeed + "이며, 속도가 " + fan.getSpeed() + " 로 변경되었습니다.");
}
@Override
public void undo() {
System.out.println("undo 커맨드 선택되었습니다.");
if (prevSpeed == Fan.OFF) { // 작업 취소 부분입니다. 이런 느낌으로 쭉쭉 추가 가능합니다.
fan.setOff();
System.out.println("이전 스피드는 " + prevSpeed + "이며, 속도가 " + fan.getSpeed() + " 로 변경되었습니다.");
} else if (prevSpeed == Fan.HIGH){
fan.setHigh();
}
}
}
public class FanOffCommand implements Command{
private final Fan fan;
private int prevSpeed;
public FanOffCommand(Fan fan) {
this.fan = fan;
}
@Override
public void execute() {
prevSpeed = fan.getSpeed();
fan.setOff();
System.out.println("off 커맨드 선택되었습니다.");
System.out.println("이전 스피드는 " + prevSpeed + "이며, 속도가 " + fan.getSpeed() + " 로 변경되었습니다.");
}
@Override
public void undo() {
System.out.println("undo 커맨드 선택되었습니다.");
if (prevSpeed == Fan.HIGH) { // 작업 취소 부분입니다. 이런 느낌으로 쭉쭉 추가 가능합니다.
fan.setHigh();
System.out.println("이전 스피드는 " + prevSpeed + "이며, 속도가 " + fan.getSpeed() + " 로 변경되었습니다.");
} else if (prevSpeed == Fan.MEDIUM){
fan.setMedium();
System.out.println("이전 스피드는 " + prevSpeed + "이며, 속도가 " + fan.getSpeed() + " 로 변경되었습니다.");
}
}
}
이런 식으로, 커맨드 구현체 자체에 prevSpeed; 라는 상태를 가지는 필드를 선언할 수 있습니다.