Notice
Recent Posts
Recent Comments
Link
티끌모아 태산
[헤드퍼스트 디자인 패턴] 6장: 커맨드 패턴 본문
아래 링크 Repository 중 내가 작성한 부분만 가져온 글이다.
https://github.com/yesman9692/Head-First-Design-Patterns/tree/main
GitHub - yesman9692/Head-First-Design-Patterns: 헤드 퍼스트 디자인 패턴 스터디 repo 입니다.
헤드 퍼스트 디자인 패턴 스터디 repo 입니다. Contribute to yesman9692/Head-First-Design-Patterns development by creating an account on GitHub.
github.com
6장: 커맨드 패턴 - 호출 캡술화 하기
만능 IOT 리모컨 만들기
- 기존 인터페이스에 공통점이 딱히 없다
- 클래스는 새로 추가될 수 있다
커맨드 패턴을 사용하면 요청하는 쪽과 처리하는 쪽을 분리할 수 있다!
커맨드 객체는 요청을 캡슐화 해준다!
- 버튼마다 커맨드 객체를 저장한다
- 리모컨은 아무 것도 몰라도 된다
- 커맨드 객체로 작업을 처리한다
예제: 객체마을 식당에서 주문을 처리해보자
- 손님이 주문서를 작성한다 한다 createOrder()
- 주문서는 주문 내용을 캡슐화한다
- 종업원이 주문을 받는다 takeOrder()
- 종업원은 그냥 아무것도 알 필요 없고, 주문서를 적당한 곳에 두면 된다
- 종업원이 주문을 주방에 전달한다 orderUp()
- 주방에선 orderUp()에 필요한 정보들을 다 알고있다
- 주방에서 음식을 만든다 makeBurger(), makeShake()
이름만 바꿔서 커맨드 패턴에 대입해보자
- 클라이언트가 커맨드 객체를 생성한다 createCommandObject()
- 커맨드 객체에서 제공하는 메소드는 execute() 하나뿐이다
- 커맨드 객체가 Invoker.setCommand()를 호출
- Invoker에 커맨드 객체를 저장한다.
- Invoker에서 커맨드 객체의 execute()를 호출한다
- 커맨드 객체는 execute()의 내용을 모른다
- 리시버에 있는 행동 메소드가 호출된다 action1(), action2()
- 커맨드 객체에는 Receiver.action() 정보가 들어있다
- 이 메소드는 행동을 캡슐화하며, 리시버에 있는 특정 행동을 처리한다
리모컨 API 디자인하기
커맨드 인터페이스 구현
public interface Command {
public void execute(); // 객체마을 식당의 orderUp과 같다
}
조명을 켤 때 필요한 커맨드 클래스 구현
- Light 클래스는 제공된 클래스로 on(), off()가 있다커맨드 객체 사용하기
public class SimpleRemoteControl { Command slot; // 커맨드를 저장할 슬롯이 1개 있다. 이 슬롯으로 1개의 기기를 제어한다 public SimpleRemoteControl() {}; public void setCommand(Command command) { // 슬롯을 가지고 제어할 명령을 설정하는 메소드 slot = command; } public void buttonWasPressed() { // 버튼을 누르면 execute() slot.execute(); } }
public class LightOnCommand implements Command { Light light; public LightOnCommand(Light light) { this.light = light; } public void execute() { light.on(); } }
메인문
public class RemoteControlTest { // 커맨드 패턴에서 클라이언트에 해당
public static void main(Stringp[] args) {
SimpleRemoteControl remote = new SimpleRemoteControl(); // 인보커 역할
Light light = new Light(); // 리시버 역할
LightOnCommand lightOn = new LightOnCommand(light); // 커맨드 객체. 리시버를 전달.
remote.setCommand(lightOn); // 인보커에 커맨드 객체를 전달
remote.buttonWasPressed();
}
}
커맨드 패턴 정의
커맨드 패턴
- 커맨드 패턴을 사용하면 요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화 할 수 잇습니다.
- 이러면 요청을 큐에 저장하거나 로그로 기록하거나 작업 취소 기능을 사용할 수 있습니다.
리모컨 코드 만들기
public class RemoteControl {
// 이 리모컨 코드는 7개의 ON/OFF 명령을 처리 할 수 있다
Command[] onCommands;
Command[] offCommands;
// 생성자는 각 ON, OFF 배열의 인스턴스를 만들고 초기화만 하면 된다
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;
}
}
// 슬롯 번호와 그 슬롯에 저장할 ON, OFF 커맨드 객체를 인자로 전달받는다
public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
// 사용자가 ON, OFF 버튼을 누르면 리모컨 하드웨어에서 각 버튼에 대응하는 메소드를 호출
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------- 리모컨 --------\n");
for(int i=0; i<offCommand.length; i++) {
stringBuffer.append("[slot " + i +"]" + onCommands[i].getClass().getName() + " " + offCommands[i].getClass.getName() + "\n");
}
return stringBuffer.toString();
}
}
커맨드 클래스 만들기
// 조명을 켜고 끄는 클래스
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
}
public class LightOffCommand implements Command {
Light light;
public LightOffCommand(Light light) {
this.light = light;
}
public void execute() {
light.off();
}
}
// 오디오를 켜고 끄는 클래스
public class StereoOnWithCDCommand implements Command {
Stereo stereo;
public StereoOnWithCDCommand(Stereo stereo) {
this.stereo = stereo;
}
public void execute() {
stereo.on();
stereo.setCD();
stereo.setVolume(10);
}
}
public class StereoOffWithCDCommand implements Command {
Stereo stereo;
public StereoOffWithCDCommand(Stereo stereo) {
this.stereo = stereo;
}
public void execute() {
stereo.off();
}
}
// 그 외의 Command 클래스들...
리모컨 테스트
public class RemoteLoader {
public static void main(String args[]) {
RemoteControl remoteConrol = new RemoteControl();
// 장치를 각자의 위치에 맞게 생성
Light livingRoomLight = new Light("Living Room");
Light kitchenLight = new Light("Kitchen");
CeilingFan ceilingFan = new CeilingFan("Living Room");
GarageDoor garageDoor = new GarageDoor("Garage");
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);
// 선풍기 켜고 끄는 커맨드 객체
CeilingFanOnCommand ceilingFanOn = new CeilingFanOnCommand(ceilingFan);
CeilingFanOffCommand ceilingFanOff = new CeilingFanOffCommand(ceilingFan);
// 차고 문 여닫는 커맨드 객체
GarageDoorUpCommand garageDoorUp = new GarageDoorUpCommand(garageDoor);
GarageDoorDownCommand garageDoorDown = new GarageDoorDownCommand(garageDoor);
// 오디오 켜고 끄는 커맨드 객체
StereoOnWithCDCommand StereoOnWithCD = new StereoOnWithCDCommand(stereo);
StereoOffCommand StereoOff = new StereoOffCommand(stereo);
remoteConrol.setCommand(0, livingRoomLightOn, livingRoomLightOff);
remoteConrol.setCommand(1, kitchenLightOn, kitchenLightOff);
remoteConrol.setCommand(2, ceilingFanOn, ceilingFanOff);
remoteConrol.setCommand(3, garageDoorUp, garageDoorDown);
remoteConrol.setCommand(4, StereoOff, StereoOff);
System.out.println(remoteConrol); // toString() 메소드 출력
remoteConrol.onButtonWasPushed(0);
remoteConrol.offButtonWasPushed(0);
remoteConrol.onButtonWasPushed(1);
remoteConrol.offButtonWasPushed(1);
remoteConrol.onButtonWasPushed(2);
remoteConrol.offButtonWasPushed(2);
remoteConrol.onButtonWasPushed(3);
remoteConrol.offButtonWasPushed(3);
remoteConrol.onButtonWasPushed(4);
remoteConrol.offButtonWasPushed(4);
}
}
출력문
------- 리모컨 --------
[slot 0] LightOnCommand LightOffCommand
[slot 1] LightOnCommand LightOffCommand
[slot 2] CeilingFanOnCommand CeilingFanOffCommand
[slot 3] GarageDoorUpCommand GarageDoorDownCommand
[slot 4] StereoOnWithCDCommand StereoOffCommand
[slot 5] NoCommand NoCommand
[slot 6] NoCommand NoCommand
거실 조명이 켜졌습니다
거실 조명이 꺼졌습니다
주방 조명이 켜졌습니다
주방 조명이 꺼졌습니다
거실 선풍기 속도가 HIGH로 설정되었습니다
거실 선풍기가 꺼졌습니다
차고 문이 열렸습니다
차고 문이 닫혔습니다
거실 오디오가 켜졌습니다
거실 오디오에서 CD가 재생됩니다
거실 오디오의 볼륨이 11로 설정되었습니다
거실 오디오가 꺼졌습니다
NoCommand 객체
NoCommand라고 출력된 부분은 뭔가요?
- NoCommand 객체는 일종의 Null 객체입니다
- Null 객체는 딱히 리턴할 객체도 없고 클라이언트가 null을 처리하지 않게 하고 싶을 때 활용하면 좋습니다
- 널 객체는 여러 디자인 패턴에서 유용하게 쓰입니다
- 그래서 널 객체를 일종의 디자인 패턴으로 분류하기도 합니다
// 메소드의 로딩 현황을 파악한 후 처리 public void onButtonWasPushed(int slot) { if (onCommands[slot] != null) { onCommands[slot].execute(); } }
//아무 일도 하지 않는 커맨드 클래스 구현
public class NoCommand implements Command {
public vod execute() {};
}
### 리모컨 API 디자인

### Undo 기능!
- OnClass의 excute()에서 on() 메소드를 호출한다면
- OnClass의 undo()를 만들어서 off() 메소드를 호출하면 된다
```java
// 조명을 켜고 끄는 클래스
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
public void undo() {
light.off();
}
}
public class LightOffCommand implements Command {
Light light;
public LightOffCommand(Light light) {
this.light = light;
}
public void execute() {
light.off();
}
public void undo() {
light.on();
}
}
하지만 좀 더 복잡한 undo를 구현하려면?
- 예를들어 선풍기 클래스에서 바람 세기가 (상, 중, 하, 꺼짐) 중 무엇이었는지 기억해야 한다면?
undo를 여러번 눌러도 동작하게 하려면 prevSpeed를 스택으로 만들자public class CeilingFanHighCommand implements Command { CeilingFan ceilingFan; int prevSpeed; // 선풍기의 속도를 저장할 변수 public CeilingFanHighCommand(CeilingFan ceilingFan) { this.ceilingFan = ceilingFan; } // 속도를 변경하기 전, 이전 속도를 저장 public void execute() { prevSpeed = ceilingFan.getSpeed(); ceilingFan.high(); } // 작업을 취소하면 prevSpeed에 알맞는 분기 실행 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(); } } }
public class CeilingFan { public static final int HIGH = 3; public static final int MEDIUM = 2; public static final int LOW = 1; public static final int OFF = 0; String location; int speed; public CeilingFan(String location) { this.location = location; speed = OFF; } public void high() { speed = HIGH; } public void medium() { speed = MEDIUM; } public void low() { speed = LOW; } public void off() { speed = OFF; } public int getSpeed() { return speed; } }
여러 동작을 한 번에 처리하기
버튼 한 개만 누르면 다 되는 기능?
- 조명 어두워지기
- 오디오와 TV 켜기
- DVD 모드로 변경
- 욕조에 물이 채우기
매크로 커맨드
// 다른 커맨드를 실행할 수 있는 커맨드
public class MacroCommand implements Command {
Command[] commands;
// Command 배열을 받아서 저장
public MacroCommand(Command commands) {
this.commands = commands;
}
// 매크로를 실행하면 각 커맨드를 순서대로 실행
public void execute() {
for (int i=0; i<commands.length; i++) {
commands[i].execute();
}
}
}
커맨드 패턴 활용하기
커맨드로 컴퓨테이션의 한 부분(리시버와 일련의 행동)을 패키지로 묶어서 객체 형태로 전달
- 오랜 시간이 지나도 그 컴퓨테이션을 호출 가능
- 다른 스레드에서 호출 가능
- 스케줄러, 스레드 풀, 작업 큐에서 사용 가능
ex) 작업 큐
- 큐 한 쪽 끝은 커맨드를 추가
- 다른 쪽 끝은 커맨드를 처리하는 스레드들이 대기
- 각 스레드는 execute()를 호출
- 완료되면 커맨드 객체를 버리고 새로운 커맨드 객체를 가져옴
- 한 스레드가 금융 계산을 하다가, 다음엔 네트워크 관련 처리를 할 수도 있다.
- 작업 큐 객체는 전혀 신경쓸 필요가 없다.
커맨드 패턴 더 활용하기
App이 다운되었을 때 복구하기
- 커맨드 패턴을 이용하여 아래 메소드 구현
- store()
- load()
- 실행 히스토리를 디스크에 기록 -> store()
- App이 다운되면 해당 히스토리 순서대로 커맨드 객체를 다시 로딩 -> load()
트랜잭션에도 활용 가능
- commit
- rollback
커맨드 패턴 핵심 정리
- 요청하는 객체와 요청을 수행하는 객체를 분리
- 이 중심에 커맨드 객체가 존재하며, 이 객체가 행동이 들어있는 리시버를 캡슐화
- 인보커는 무언가 요청할 때 커맨드 객체의 execute()메소드를 호출
- execute() 메소드는 리시버에 있는 행동을 호출
- 커맨드는 인보커를 매개변수화 할 수 있다
- 커맨드 패턴으로 작업 취소 기능으로 구현 가능
- 매크로 커맨드는 여러 개의 커맨드를 한 번에 호출하는 커맨드
- 요청을 스스로 처리하는 '스마트 커맨드' 객체를 사용하는 경우도 종종 존재
- 로그 및 트랜잭션 시스템 구현 가능
'독서 > 헤드퍼스트 디자인패턴' 카테고리의 다른 글
[헤드퍼스트 디자인패턴] 8장: 템플릿 메소드 패턴 - 알고리즘 캡슐화하기 (0) | 2023.09.26 |
---|---|
[헤드퍼스트 디자인 패턴] 4장: 팩토리 패턴 (0) | 2023.09.13 |
[헤드퍼스트 디자인 패턴] 2장: 객체들에게 연락 돌리기 - 옵저버 패턴 (0) | 2023.09.13 |
[헤드퍼스트 디자인 패턴] OT (0) | 2023.09.13 |