* 본 포스팅은 한빛미디어의 헤드퍼스트 디자인 패턴을 공부한 내용을 정리한 글입니다.
저의 부족한 생각과 주관으로 틀린 내용이 있을 수 있으니,
자세한 내용이 궁금하시다면 해당 책을 읽어보시길 추천드립니다.
1.커맨드 패턴이란
✏️커맨드 패턴(Command Pattern)
객체의 행위method)를 캡슐화함으로써 주어진 여러 기능을 실행하지만 재사용성이 높은 클래스를 설계하는 패턴이다.
커맨드 객체는 통해 일련의 행동을 특정 리시버와 연결함으로써 요청을 캡슐화한 것입니다.
그렇게 되면, 호출시에 어떻게 처리되는지는 신경을 쓸 필요가 없고 처리가 된다라는 사실만 알 수 있습니다.
전등을 키는 경우를 예로 들어보겠습니다.
전등도 사실 그냥 켜지는게 아니라 전기를 끌어와서 형광등의 전력량에 알맞게 변환하고, ...@#$% , (이하생략
사실 불을 키는 우리는 해당 과정을 알 필요가 없죠? 버튼만 누르면 불이 켜지는데 말이죠!
2. 구조
- Command
모든 커맨드 객체에서 구현해야하는 인터페이스로, excute()와 undo()로 구성됩니다. - ConcreteCommand
커맨드 인터페이스를 구현해 특정 행동과 리시버를 연결합니다. (execute() - 리시버의 메소드를 호출해 요청작업 수행) - Invoker
커맨드 객체를 setCommand()를 통해 저장하며, execute()를 호출해 특정 작업을 수행하도록 커맨트 객체를 호출 - Receiver
요구사항에 대해 처리방법을 가지고 있는 객체 - Client
ConcreteCommand를 생성하고 Receiver를 설정합니다.
3. 구현해보기
커맨드 패턴을 홈 오토메이션 리모콘 API를 디자인하면서 구현해 보도록 하겠습니다.
3-1. Command
public interface Command {
public void execute();
}
3-2. ConcreateCammand
public class LightOnCommand implements Command {
Light light;
//set receiver
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
}
3-3. Invoker
public class RemoteControl {
Command[] onCommands;
Command[] offCommands;
//on-off쌍의 버튼이 7쌍이 있다고 해 봅시다.
public Remotecontrol() {
onCommands = new Command[7];
offCommands= new Command[7];
}
// 커맨드 객체를 각 리모콘의 슬롯에 저장합니다.
public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
public void onButonWasPushed(int slot) {
onCommands[slot].execute();
}
public void offButonWasPushed(int slot) {
offCommands[slot].execute();
}
}
3-4. Do-test
public class RemoteLoader {
public static void main(String[] args) {
Remotecontrol remoteControl = new RemoteControl();
//receiver object
Light livingRoomLight = new Light("Living Room");
//command object
LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
//save command object to invoker
remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
//test - request work
remoteControl.onButtonWasPushed(0);
remoteControl.offButtonWasPushed(0);
}
}
❗️여기서 잠깐!
위 invoker(RemoreContol)에선 버튼을 각 7개씩으로 정의했습니다.
그리고 3-4. Do-test에서 커맨드 객체를 0번지 버튼에만 저장하고 나머진 비워 두었습니다.
만약 RemoteContol에서 7개의 슬롯을 초기화할때 비워든 상태로 시작했다면,
일일이 버튼을 누를때마다 커맨드객체가 로딩되어있는지를 확인해 주어야 합니다.
public void onButtonWasPushed(int slot){
if(onCaommands[slot] != null{
onCommands[slot].execute();
}
}
왜냐면 커맨드 객체가 로딩되어 있지 않은데 excute()를 호출하면 NPE가 발생하기 때문입니다.
그래서 해당경우엔 애초에 리모콘 슬롯을 초기화할때 아무 동작이 없는 NoCommand객체를 주입해주고 시작하면 됩니다.
아래와같이 RemoteConrol의 생성자 코드를 수정해주면 됩니다.
public class NoCommand implements Command(){
public void excute(){}
}
public class RemoteControl {
//...
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;
}
}
//...
}
❓리시버 없이 커맨드 객체에서 exceute()를 구현하면 안될까?
요청을 스스로 처리하는 커맨드를 '스마트 커맨드'라고 합니다.
종종 사용하지만, 인보커와 리시버 분리가 어렵고 리시버로 커맨드를 매개변수화할 수가 없습니다.ㅏㅁ
4. API 문서
커맨트 패턴을 사용해 작성한 리모콘 API의 구조는 아래와 같습니다.
- Command
모든 RemoteControl 클래스용 커맨드 객체는 Command를 구현해야 합니다.
커맨드 객체는 일련의 행동을 캡슐화 하며, 리모콘은 해당 행동을 execute(), undo()를 통해 호출합니다. - LightOnCommand, LightOffCommand
리모콘을 눌렀을때 호출되는 행동입니다. Command 인터페이스를 구현한 커맨드 객체로 구현됩니다.
머캔드 객체에는 업체에서 제공한 클래스 인스턴스의 레퍼런스가 존재하여, execute()-undo()로 구현되어 있습니다. - RemoteControl
버튼마다 하나의 커맨드 객체를 할당해서 관리합니다.
버튼이 눌리면 버튼에 해당하는 ButtonWasPushed()가 호출되어 execute()를 호출합니다. - Light
협력업체가 제공한 클래스로, Light 클래스를 예로 들었습니다. - RemoteLoader
리모콘 슬롯에 로딩되는 일련의 커맨드 객체를 생성합니다.
각 커맨드 객체에는 홈 오토메이션 장치로 전송되는 요청이 캡슐화되어 있습니다.
5. 심화편 - 람다
public class RemoteLoader {
public static void main(String[] args) {
Remotecontrol remoteControl = new RemoteControl();
Light livingRoomLight = new Light("Living Room");
LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
...
}
}
Java8이후로 지원되는 람다 표현식을 사용해,
객체 생성 대신에 함수 객체를 사용하면 코드가 단순해 질 수 있습니다.
다만, Command 인터페이스에 추상 메서드가 하나일때만 사용할 수 있는 코드입니다.
public class RemoteLoader {
public static void main(String[] args) {
Remotecontrol remoteControl = new RemoteControl();
Light livingRoomLight = new Light("Living Room");
remoteControl.setCommand(0, () -> livingRoomLight.on(), () -> livingRoomLight.off());
...
}
}
6. Undo 기능 추가
위 코드에서 Command 인터페이스에 undo()도 마저 작성해 보도록 하겠습니다.
첫째로, 단순히 on-off인 경우엔 눌린 버튼에 반대대되는 커맨드객체를 undoCommand에 로딩하면 됩니다.
public class RemoteControl {
Command[] onCommands;
Command[] offCommands;
Comand UndoCommand;
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;
}
undoCommand = noCommand;
}
public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
public void onButonWasPushed(int slot) {
onCommands[slot].execute();
undoCommand = offCommands[slot];
}
public void offButonWasPushed(int slot) {
offCommands[slot].execute();
undoCommand = onCommands[slot];
}
public void indoButtonWasPushed(){
undoCommand.undo();
}
}
둘째로, 선풍기의 풍량조절과 같은 수치조절 기능이 존재한다면,
ConcreateCommand 클래스가 이전 상태의 풍량을 저장하는 변수를 갖도록 해 undo()를 구현하면 됩니다.
public class CeilingFanHighCommand implements Command {
CeilingFan ceilingFan;
int prevSpeed;
public CeilingFanHighCommand (CeilingFan ceilingFan) {
this.ceilingFan= ceilingFan;
}
public void execute() {
prevSpeed = ceilingFan.getSpeed();
light.high();
}
public void undo() {
if (prevSpeed == CeilingFan.HIGH) {
ceilingFan.high();
} else if (prevSpeed == CeilingFan.MEDIUM) {
ceilingFan.medium();
} else if (prevSpeed == CeilingFan.LOW) {
ceilingFan.low();
} else if (prevSpeed == CeilingFan.OFF) {
ceilingFan.off();
}
}
}
7. 매크로 커맨드
다른 커맨드를 실행하는 커맨드를 매크로 커맨드라고 합니다.
public class MacroCommand implements Command {
Command[] commands;
public MacroCommand(Command[] commands) {
this.commands = commands;
}
public void execute() {
for (int i = 0; i < commands.length; i++) {
commands[i].execute();
}
}
}
각 커맨드들을 만들어 아래처럼 넣으면 여러 커맨드를 순차적으로 실행하는 매크로 커맨드를 실행할 수 있습니다.
Command[] partyOn = [lightOn, stereoOn, tvOn, hottubOn};
Command[] partyoff = [lightoff, stereoOff, tvOff, huttubOff};
MacroCommand partyOnMacro = new MacroCommand(partyOn);
MacroCommand partyOffMacro = new MacroCommand)partyOff);
8. 마무리하며
- 커맨드 패턴을 사용하면 요청객체와 요청을 수행하는 객체를 분리할 수 있습니다.
- 커맨드 객체는 행동이 들은 리시버를 캡슐화합니다.
- Invoker는 요청시 커맨드객체의 execute()를 호출하는데, execute()는 리시버에 있는 행동을 호출합니다.
- 커맨드로 컴퓨테이션(computation)의 리시버와 일련의 행동을 패키지로 묶어서 일급 객체 형태로 전달할 수 있습니다.
그래서 클라이언트 애플리케이션에서 나중에 컴퓨테이션을 호출하거나 다른 스레드에서 호출이 가능합니다.
이점을 활용하면 스케줄러나 스레드 풀, 작업 큐에 적용할 수 있다. - 애플리케이션이 다운되었을 때 그 행동을 다시 호출해서 복구하는 기능을 커맨드 객체에 store()와 load() 메소드를 추가하는 방식으로 기능 구현이 가능합니다.
예를들어, 스프레드시트는 매번 데이터가 변경될 때마다 저장하지 않고 로그에 기록하는 방식으로 복구 시스템을 구축하였습니다.
'개발공부 > Code design' 카테고리의 다른 글
헤드퍼스트 디자인 패턴 05.싱글턴(Singleton) 패턴 (0) | 2022.11.22 |
---|---|
헤드퍼스트 디자인 패턴 04.팩토리(Factory) 패턴 - 팩토리 메서드 (0) | 2022.09.25 |
헤드퍼스트 디자인 패턴 04.팩토리(Factory) 패턴 - 심플 팩토리 (0) | 2022.09.25 |
헤드퍼스트 디자인 패턴 03.데코레이터(Decorator) 패턴 (0) | 2022.08.08 |
헤드퍼스트 디자인 패턴 2장. 옵저버(Observer)패턴 (0) | 2022.07.17 |