Please enable JavaScript to view the comments powered by Disqus.커맨드 패턴 - command pattern 이란 무엇인가?
Search
📥

커맨드 패턴 - command pattern 이란 무엇인가?

태그
Design Pattern
공개여부
작성일자
2021/01/12
메소드 호출을 캡슐화 하여 계산의 각 과정부분들을 결정화 시킬 수 있다. 캡슐화된 method 호출을 로그 기록용으로 저장하거나 취소 기능을 구현하고 재사용 할 수 있다. 이 기능을 구현하면서 각 도메인의 핵심적인 로직을 몰라도 되는 변경에 닫혀있고, 확장에 열려있는 코드를 작성해보자.
이 컨텐츠는 Head First의 Design Pattern 책의 6번 챕터인 커맨트 패턴을 정리한 내용입니다
코드를 모두 입력해두면 컨텐츠 길이가 의미없이 길어질것 같아 Github 에 코드를 올려두었습니다

요구사항

프로그래밍이 가능한 리모콘에 7개의 소켓이 있고, 각 소켓에 필요한 프로그램을 연결한다
각 프로그램을 통해 특정 기능을 on/off 할 수 있다.
이렇게 다양한 클래스가 있는데 어떻게 하나의 리모콘에 연결할 수 있을까?

구현 방법

1.
리모콘은 제작 업체가 제공한 클래스를 몰라야한다
2.
작업을 요청한쪽과 그 작업을 처리한 쪽을 분리시킨다.
특정 작업 요청을 캡슐화 하자
3.
사용자가 버튼을 눌렀을 때 작업을 처리하여 리모콘은 자세한 내용을 모르게 한다.

식당에서 발생하는 event 로 이해하는 command pattern

고객 → 웨이트리스에게 주문 → 주문서 작성 → 주방장에게 주문 전달
웨이트리스 입장
어떤 주문인지 조차도 몰라도 된다.
누가 식사를 준비할지 몰라도 된다.
주방장 입장
주문서를 읽고 필요한 메뉴만 준비하면 된다.
누가 주문했는지 알 필요가 없다
Search
비교
식당
커맨드 패턴
execute()
client 객체
invoker 객체
receiver 객체
setCommand()
COUNT6

Command 객체

command 객체는 오로지 execute() 메소드만 제공한다.
public interface Command { void execute(); }
Java
복사
public class LightCommand implements Command { private Light light; public LightCommand(Light light) { this.light = light; } @Override public void execute() { light.on(); } }
Java
복사
public class Light { public void on() { System.out.println("turn on light"); } public void off() { System.out.println("turn off light"); } }
Java
복사

Command 객체 사용하기

// Invoker 역할을 수행한다. public class SimpleRemoteControl { private Command slot; public void setSlot(Command command) { this.slot = command; } public void buttonWasPressed() { slot.execute(); } }
Java
복사

Command 패턴의 정의

커맨드 객체는 일련의 행동을 특정 리시버와 연결시켜 요구사항을 캡슐화 한다
행동과 리시버를 한 객체에 집어넣고, execute() 메소드 하나만 외부에 공해한다.
리시버에서 execute() 만 가지고 일련의 작업을 수행한다.
외부에선 누가 리시버이며, 그 내부에서 무엇을 하는지 알 수 없다.
client 는 ConcreteCommand 를 생성하며 Receiver 를 설정한다
Receiver 는 요구사항 수행을 위해 어떤 일을 처리해야 하는지 알고있으며, 요청을 처리한다
ConcretCommand 는 action과 receiver 를 연결하며 Invoker 에서 execute() 호출을 통해 요청을 하면 리시버에 있는 메소드를 호출하면서 필요한 업무를 수행한다.
Invokerexecute() 를 호출함으로써 커맨드 객체가 어떤 작업을 수행해야할지 요구한다
Command 는 모든 커맨드 객체가 구현해야 한다.
execute() 를 사용해 receiver 에게 특정 작업을 수행할것을 요구한다
다시 리모콘으로 돌아가자 7개의 소켓이 각각 무엇인지 어떤 명령을 수행해야하는지 어떻게 알아야 할까?

슬롯에 명령 할당하기

각 슬롯에 알맞는 데이터를 추가해보자
RemoteLoader
public class NoCommand implements Command { @Override public void execute() { System.out.println("no command!"); } }
Java
복사
이와 같은 NoCommand 를 사용하지 않는다면 onButtonWasPushed 는 매번 null 체크를 해야한다.

작업 취소기능 (Undo)

public interface Command { public void execute(); public void undo(); }
Java
복사
undo()Command Interface 에 추가한다 (하나하나 수정해야 함 ㅠㅠ)
@RequiredArgsConstructor public class StereoOffWithCdCommand implements Command { private final Stereo stereo; @Override public void execute() { stereo.off(); } @Override public void undo() { stereo.on(); stereo.setCd(); stereo.setVolume(11); } }
Java
복사
이와같이 execute 에 딱 반대되는 행동을 수행한다. 그렇다면 Invoker 는 다음과 같다.
/** * Invoker with Undo */ public class RemoteControlWithUndo extends RemoteControl { private Command undoCommand; public RemoteControlWithUndo() { super(); undoCommand = new NoCommand(); } @Override public void onButtonWasPushed(int slot) { super.onButtonWasPushed(slot); undoCommand = onCommands[slot]; } @Override public void offButtonWasPushed(int slot) { super.offButtonWasPushed(slot); undoCommand = offCommands[slot]; } public void undoButtonWasPushed() { undoCommand.undo(); } }
Java
복사

Macro Command 사용 방법

매크로 커맨드란? Command 객체에서 execute() 를 실행할 때 n 개의 command 를 한꺼번에 실행한다.
@RequiredArgsConstructor public class MacroCommand implements Command { // final 로 사용한 이유는 생성자 사용을 강제하기 위함이다. private final Command[] commands; @Override public void execute() { Stream.of(commands) .forEach(Command::execute); } @Override public void undo() { Stream.of(commands) .forEach(Command::undo); } }
Java
복사
MacroCommand 를 사용하면 기존에 사용했던 RemoteControl 은 그대로 재사용이 가능하다
하지만, n 개의 command 를 주입받아 사용할 수 있다.
public static void main(String[] args) { MacroControl macroControl = new MacroControl(); Light livingRoom = new Light("livingRoom"); Light kitchenLight = new Light("kitchen"); CeilingFan ceilingFan = new CeilingFan("Living Room"); Stereo stereo = new Stereo("Living Room"); LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoom); LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoom); LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight); LightOffCommand kitchenLightOff = new LightOffCommand(kitchenLight); CeilingFanOn ceilingFanOn = new CeilingFanOn(ceilingFan); CeilingFanOff ceilingFanOff = new CeilingFanOff(ceilingFan); StereoOnWithCdCommand stereoOnWithCdCommand = new StereoOnWithCdCommand(stereo); StereoOffWithCdCommand stereoOffWithCdCommand = new StereoOffWithCdCommand(stereo); Command[] partyOn = { livingRoomLightOn, kitchenLightOn, ceilingFanOn, stereoOnWithCdCommand}; Command[] partyOff = { livingRoomLightOff, kitchenLightOff, ceilingFanOff, stereoOffWithCdCommand}; MacroCommand partyOnMacro = new MacroCommand(partyOn); MacroCommand partyOffMacro = new MacroCommand(partyOff); macroControl.setCommand(0, partyOnMacro, partyOffMacro); }
Java
복사
이와 같이 command[] 배열로 받아 MacroCommand 를 생성하고 이것을 Invoker 에 제공하면 여러 command 를 마치 매크로 처럼 사용할 수 있다.

커맨드 패턴의 활용: 요청을 큐에 저장하자

커맨드 패턴을 구현한 객체들을 큐에 저장한다
큐 한쪽 끝은 command 를 추가하고, 반대쪽 끝에서는 execute() 를 실행하기 위한 Thread 들이 대기하고 있는 것이다.
큐를 이와같이 준비하면 command 객체와 그 객체 안에 있는 execute 만 고민하면 되기 때문에 큐에 어떠한 작업이 와도 상관 없다.
금융 작업을 하다가 네트워크 작업을 해도 되고, 다운로드 작업을 수행해도 된다

Summary

command pattern 을 이용하면 요청하는 객체와 그 요청을 수행하는 객체를 분리할 수 있다.
분리 시키는 과정의 중심에 Command 객체가 존재한다 (식당 예제의 주문서)
CommandReceiver 를 캡슐화 한다.
InvokerCommand 를 통해서 execute() 함수를 호출한다.
execute() 커맨드는 Command 를 확장하여 작업 취소 기능을 구현할 수 있다.