📥

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

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

요구사항

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

구현 방법

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

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

Screen_Shot_2021-01-12_at_16.05.21.png
고객 → 웨이트리스에게 주문 → 주문서 작성 → 주방장에게 주문 전달
웨이트리스 입장
어떤 주문인지 조차도 몰라도 된다.
누가 식사를 준비할지 몰라도 된다.
주방장 입장
주문서를 읽고 필요한 메뉴만 준비하면 된다.
누가 주문했는지 알 필요가 없다
Screen_Shot_2021-01-12_at_16.10.21.png
비교
Search
식당
커맨드 패턴
주방장
Open
execute()
orderUp()
Open
client 객체
주문서
Open
invoker 객체
손님
Open
receiver 객체
takeOrder()
Open
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() 만 가지고 일련의 작업을 수행한다.
외부에선 누가 리시버이며, 그 내부에서 무엇을 하는지 알 수 없다.
Screen_Shot_2021-01-12_at_16.35.51.png
client 는 ConcreteCommand 를 생성하며 Receiver 를 설정한다
Receiver 는 요구사항 수행을 위해 어떤 일을 처리해야 하는지 알고있으며, 요청을 처리한다
ConcretCommand 는 action과 receiver 를 연결하며 Invoker 에서 execute() 호출을 통해 요청을 하면 리시버에 있는 메소드를 호출하면서 필요한 업무를 수행한다.
Invokerexecute() 를 호출함으로써 커맨드 객체가 어떤 작업을 수행해야할지 요구한다
Command 는 모든 커맨드 객체가 구현해야 한다.
execute() 를 사용해 receiver 에게 특정 작업을 수행할것을 요구한다
다시 리모콘으로 돌아가자 7개의 소켓이 각각 무엇인지 어떤 명령을 수행해야하는지 어떻게 알아야 할까?

슬롯에 명령 할당하기

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

작업 취소기능 (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 들이 대기하고 있는 것이다.
Screen_Shot_2021-01-12_at_18.24.29.png
큐를 이와같이 준비하면 command 객체와 그 객체 안에 있는 execute 만 고민하면 되기 때문에 큐에 어떠한 작업이 와도 상관 없다.
금융 작업을 하다가 네트워크 작업을 해도 되고, 다운로드 작업을 수행해도 된다

Summary

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