메소드호출을 캡슐화하면 계산 과정의 각 부분을 결정화할 수 있기에 계산하는 코드를 호출한 객체는 그 일이 어떤 식으로 처리되는지 전혀 신경 쓸 필요가 없어진다.
커맨드 패턴
커맨드 패턴을 사용하면 요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화할 수 있다. 이러면 요청을 큐에 저장하거나 로그로 기록하거나 작업 취소 기능을 사용할 수 있다.
커맨드 객체는 일련의 행동을 특정 리시버와 연결함으로써 요청을 캡슐화한것이다.
- 이를 위해 행동과 리시버를 한 객체에 넣고,
excute()
라는 메소드 하나만 외부에 공개하는 방법을 써야 한다. - 이 메소드 호출에 따라 리시버에서 일련의 작업을 처리하며, 밖에서 볼 때는 어떤 객체가 리시버 역할을 하는지, 그 리시버가 어떤 일을 하는지 알 수 없다.
명령으로 객체를 매개변수화 할 수 있다.
- 특정 인터페이스만 구현되어 있다면 그 커맨드 객체에서 실제로 어떤 일을 하는지 신경 쓸 필요가 없다.
기본적인 커맨드 패턴을 조금만 확장하면 큐와 로그를 구현하거나 작업 취소 하는 방법으로 활용될 수 있다.
기본적인 커맨드 패턴을 제대로 사용할 수 있다면 메타 커맨드 패턴(Meta Command Pattern)도 어렵지 않게 구현할 수 있다.
- 메타 커맨드 패턴을 사용하면 여러 개의 명령을 매크로로 한번에 실행할 수 있다.
커맨드 패턴은 어떤 것을 요구하는 객체와 그 요구를 받아들이고 처리하는 객체를 분리하는 객체지향 디자인 패턴의 한 모델이라고 볼 수 있다.
- 어떤 작업을 요청하는 쪽과 그 작업을 처리하는 쪽을 분리할 수 있다.
- 커맨드 객체는 특정 객체에 관한 특정 작업 요청을 캡슐화해준다.
- 그렇게 객체를 분리하면 패턴이 실제로 어떻게 돌아가는지 파악하기가 조금 어려울 수 있다.
flowchart LR
a(("클라이언트createCommandObject()"))
b(("커맨드excute()"))
c(("인보커setCommand()"))
d(("커맨드execute()"))
e(("리시버action1()action2()"))
a-- 1. createCommandObject() --> b
b-- 2. setCommand() --> c
a-. 3 .-> c
c-- excute() --> d
d-- action1(), action2() --> e
- 클라이언트는 커맨드 객체를 생성해야 한다.
- 커맨드 객체는 리시버에 전달할 일련의 행동으로 구성된다.
- 커맨드 객체에는 행동과 리시버(Receiver)의 정보가 같이 들어있다.
- 커맨드 객체에서 제공하는 메소드는
excute()
하나 뿐이다.- 이 메소드는 행동을 캡슐화하며, 리시버에 있는 특정 행동을 처리한다.
- 클라이언트는 인보커(Invoker) 객체의
setCommand()
메소드를 호출하는데, 이때 커맨드 객체를 넘겨준다.- 그 커맨드 객체는 나중에 쓰이기 전까지 인보커 객체에 보관된다.
- 인보커에서 커맨드 객체의
excute()
메소드를 호출하면 리시버에 있는 행동 메소드가 호출된다.
인보커 로딩
- 클라이언트에서 커맨드 객체 생성
setCommand()
를 호출해서 인보커에 커맨드 객체를 저장- 나중에 클라이언트에서 인보커에게 그 명령을 실행하라고 요청
일단 어떤 명령을 인보커에 로딩한 다음 한번만 작업을 처리하고 커맨드 객체를 지우도록 할 수도 있고, 저장해 둔 명령을 여러 번 수행하게 할 수도 있다.
기본 구조
classDiagram
direction LR
class Client
class Invoker {
setCommand()
}
class Receiver {
action()
}
class Command {
<< interface >>
excute()*
undo()*
}
class ConcreteCommand {
execute()
undo()
}
Client --> Invoker
Client --> Receiver
Client --> ConcreteCommand
Invoker --> Command
Receiver <-- ConcreteCommand
Command <.. ConcreteCommand
커맨드 패턴 활용하기
커맨드로 컴퓨테이션(computation)의 한 부분(리시버와 일련의 행동)을 패키지로 묶어서 일급 객체 형태로 전달할 수 있다.
- 클라이언트 애플리케이션에서 커맨드 객체를 생성한 뒤 오랜 시간이 지나도 그 컴퓨테이션을 호출할 수 있게 된다.
- 다른 스레드에서 호출할 수도 있다.
이러한 특성을 활용하여 커맨드 패턴을 수케줄러나 스레드 풀, 작업 큐와 같은 다양한 작업에 적용할 수 있다.
작업 큐
큐 한 쪽 끝은 커맨드를 추가할 수 있도록 되어있고, 다른 쪽 끝에는 커맨드를 처리하는 스레드들이 대기하고 있다.
각 스레드는 우선 execute()
메소드를 호출하고 호출이 완료되면 커맨드 객체를 버리고 새로운 커맨드 객체를 가져온다.
작업 큐 클래스는 계산 작업을 하는 객체들과 완전히 분리되어 있고, 한 스레드가 처리를 하다 네트워크로 뭔가를 내려받을 수도 있다고 하더라도, 큐에 커맨드 패턴을 구현하는 객체를 넣으면 그 객체를 처리하는 스레드가 생기고 자동으로 excute()
가 호출되므로 작업 큐 객체는 전혀 신경쓸 필요가 없다.
커맨드 패턴 더 활용하기
어떤 어플래케이션은 모든 행동을 기록해 두었다가 어플리케이션이 다운되었을 때 그 행동을 다시 호출해서 복구할 수 있어야 한다.
커맨드 패턴을 사용하면 store()와 load() 메소드를 추가해서 이런 기능을 구현할 수 있다.
- 자바에서는 직렬화로 구현할 수도 있지만 관련된 제약 조건으로 인해 쉽지 않다.
classDiagram
class Command {
<< interface >>
execute()*
undo()*
store()*
load()*
}
flowchart LR
a((Invoker))
b((Command1))
c((Command2))
d((Command3))
e[(storage)]
a-- 1. execute() -->b
a-- 2. execute() -->c
a-- 3. execute() -->d
b-- store -->e
c-- store -->e
d-- store -->e
flowchart LR
e[(storage)]
b((Command1))
c((Command2))
d((Command3))
a((Invoker))
e-- load -->b
e-- load -->c
e-- load -->d
b-. "1. execute()" .-a
c-. "2. execute()" .-a
d-. "3. execute()" .-a
만능 IOT 리모컨
커맨드 인터페이스 구현
커맨드 객체는 모두 같은 인터페이스를 구현해야 한다.
그 인터페이스에는 메소드가 하나 뿐이며 일반적으로 excute()
라는 이름을 쓴다.
1
2
3
| public interface Command {
public void excute();
}
|
커맨드 클래스 구현
classDiagram
class Light {
on()
off()
}
1
2
3
4
5
6
7
8
9
10
11
| public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void excute() {
light.on();
}
}
|
커맨드 객체 사용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| public class SimpleRemoteControl {
/**
* 커맨드를 저장할 슬롯이 1개 있다.
* 이 슬롯으로 1개의 기기를 제어한다.
*/
Command slot;
public SimpleRemoteControl() {};
/**
* 슬롯을 가지고 제어할 명령을 설정한다.
* 리모컨 버튼의 기능을 바꾸고 싶다면 해당 메소드를 사용해서 얼마든지 바꿀 수 있다.
*/
public void setCommand(Command command) {
slot = command;
}
/**
* 버튼을 누르면 메소드가 호출되며, 슬롯에 연결된 커맨드 객체의 excute() 메소드만 호출하면 된다.
*/
public void buttonWasPressed() {
slot.execute();
}
}
|
간단한 테스트 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| /**
* 커맨드 패턴에서 클라이언트에 해당하는 부분
*/
public class RemoteControlTest {
public static void main(String[] args) {
/**
* remote 변수가 인보커 역할을 한다.
* 필요한 작업을 요청할 때 사용할 커맨드 객체를 인자로 전달받는다.
*/
SimpleRemoteControl remote = new SimpleRemoteControl();
/**
* 요청을 받아서 처리할 리시버인 Light 객체를 생성한다.
*/
Light light = new Light();
/**
* 커맨드 객체를 생서한다. 이때 리시버를 전달해준다.
*/
LightOnCommand lightOn = new LigthOnCommand(light);
/**
* 커맨드 객체를 인보커에게 전달해준다.
*/
remote.setCommand(lightOn);
remote.buttonWasPressed();
}
}
|
리모컨 코드 만들기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| public class RemoteControl {
Command[] onCommands;
Command[] offCommands;
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;
}
}
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();
}
public String toString() {
StringBuffer stringBuff = new StringBuffer();
stringBuff.append("\n----- 리모컨 -----\n");
for (int i = 0; i < onCommands.length; i++) {
stringBuff.append(
"[slot " + i + "] " +
onCommands[i].getClass().getName() +
" " +
offCommands[i].getClass().getName() + "\n"
);
}
return stringBuff.toString();
}
}
|
NoCommand 객체
NoCommand
객체는 일종의 널 객체이다.
널 객체는 딱히 리턴할 객체도 없고 클라이언트가 null을 처리하지 않게 하고 싶을때 활용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public class NoCommand implements Command {
public void execute() {}
}
public class RemoteControl {
/**
* NoCommand 객체가 없는 경우 처리 예시
*/
public void onButtonWasPushed(int slot) {
if (onCommands[slot] != null) {
onCommands[slot].execute();
}
}
}
|
만능 IOT 리모컨의 경우 명령이 아직 할당되지 않은 부분에 NoCommand 객체를 넣어 execute()
메소드가 호출되어도 문제가 생기지 않도록 했다.
작업 취소 기능 추가하기
Command 클래스에 상태를 추가하여 작업을 취소 기능을 쉽게 추가할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| public interface Command {
public void execute();
public void undo(); // 새로 추가
}
public class LightOnCommand implements Command {
// ...
public void undo() {
light.off();
}
}
public class LightOffCommand implements Command {
// ...
public void undo() {
light.on();
}
}
public class RemoteControlWithUndo {
// ...
Command undoCommand;
public RemoteControlWithUndo() {
// ...
undoCommand = noCommand;
}
// ...
public void onButtonWasPushed(int slot) {
onCommands[slot].execute();
undoCommand = onCommands[slot];
}
public void offButtonWasPushed(int slot) {
offCommands[slot].execute();
undoCommand = offCommands[slot];
}
public void undoButtonWasPushed() {
undoCommand.undo();
}
}
|
상태를 활용한 작업 취소 기능
classDiagram
class CeilingFan {
high()
medium()
low()
off()
getSpeed()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
| 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;
}
}
public class CeilingFanHighCommand implements Command {
CeilingFan ceilingFan;
int prevSpeed;
public CeilingFanHighCommand(CeilingFan ceilingFan) {
this.ceilingFan;
}
public void execute() {
prevSpeed = ceilingFan.getSpeed();
ceilingFan.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();
}
}
}
|
매크로 커맨드로 여러 동작 한번에 처리하기
1
2
3
4
5
6
7
8
9
10
11
12
13
| 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();
}
}
}
|
정리
- 커맨드패턴
- 요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화 할 수 있다.
- 이러면 요청을 큐에 저장하거나 로그로 기록하거나 작업 취소 기능을 사용할 수 있다.
- 커맨드 패턴을 사용하면 요청하는 객체와 요청을 수행하는 객체를 분리할 수 있다.
- 분리하는 과정의 중심에는 커맨드 객체가 있으며, 객체가 행동이 들어있는 리시버를 캡슐화한다.
- 인보커는 무언가 요청할 때 커맨드 객체의
execute()
메소드를 호출하면된다.execute()
메소드는 리시버에 있는 행동을 호출한다.
- 커맨드는 인보커를 매개변수화 할 수 있다.
- 실행 중 동적으로 매개변수화를 설정할 수도 있다.
execute()
메소드가 마지막으로 호출되기 전의 상태로 되돌리는 작업 취소 메소드를 구현하면 커맨드 패턴으로 작업 취소 기능을 구현할 수도 있다.- 매크로 커맨드는 커맨드를 확장해서 여러 개의 커맨드를 한 번에 호출할 수 있게 해주는 가장 간편한 방법이다.
- 매크로 커맨드로도 어렵지 않게 작업 취소 기능을 구현할 수 있다.
- 프로그래밍을 하다 보면 요청을 스스로 처리하는 ‘스마트’ 커맨드 객체를 사용하는 경우도 있다.
- 커맨드 패턴을 활용해서 로그 및 트랜잭션 시스템을 구현할 수 있다.