Skip to content

Commit 45cbc50

Browse files
committed
커맨트 패턴 초안
1 parent 1537938 commit 45cbc50

7 files changed

Lines changed: 307 additions & 0 deletions

File tree

ch06/Command.ipynb

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
{
2+
"cells": [
3+
{
4+
"metadata": {},
5+
"cell_type": "markdown",
6+
"source": [
7+
"# CH06. 호출 캡슐화하기 - 커맨드 패턴(Command Pattern)\n",
8+
"\n",
9+
"## 1. 개요\n",
10+
"커맨드 패턴은 **요청(Request)을 객체로 캡슐화**하여, 호출자(Invoker)와 수신자(Receiver)를 분리(Decoupling)하는 패턴입니다.\n",
11+
"\n",
12+
"### 비유: 식당의 주문 시스템\n",
13+
"![img.png](img.png)\n",
14+
"- 손님 (Client): 주문을 결정하고 웨이트리스에게 전달합.\n",
15+
"- 주문서 (Command): 주문 내용이 적힌 객체. 주방장에게 무엇을 할지 알려주는 인터페이스 역할.\n",
16+
"- 웨이트리스 (Invoker): 주문서를 받아서 주방장에게 전달 (주방장이 구체적으로 뭘 하는지는 몰라도 됨.)\n",
17+
"- 주방장 (Receiver): 실제로 요리를 하는 객체.\n",
18+
"\n",
19+
"## 2. 클래스 구성\n",
20+
"![img_1.png](img_1.png)\n",
21+
"![img_5.png](img_5.png)\n",
22+
"- Client : ConcreteCommand를 생성하고 Receiver를 설정합니다.\n",
23+
"- Invoker :\t커맨드 객체를 저장하고 있으며, 적절한 시점에 execute()를 호출합니다.\n",
24+
"- Command :\t모든 커맨드 객체가 구현해야 하는 인터페이스. 보통 execute() 메서드 하나만 가집니다.\n",
25+
"- ConcreteCommand :\t실제 동작과 리시버를 연결합니다. execute()가 호출되면 리시버의 메서드를 호출합니다.\n",
26+
"- Receiver : 실제로 일을 하는 객체 (예: 전등, 오디오, 차고 문).\n"
27+
],
28+
"id": "698a0f1c0261b1b4"
29+
},
30+
{
31+
"metadata": {
32+
"executionRelatedData": {
33+
"compiledClasses": [
34+
"Line_6_jupyter",
35+
"Line_11_jupyter"
36+
]
37+
},
38+
"ExecuteTime": {
39+
"end_time": "2026-02-07T09:18:01.624520Z",
40+
"start_time": "2026-02-07T09:18:01.506286Z"
41+
}
42+
},
43+
"cell_type": "code",
44+
"source": [
45+
"// Receiver (수신자) : 리모컨이 제어할 실제 가전제품\n",
46+
"\n",
47+
"class Light(val location: String) {\n",
48+
" fun on() = println(\"$location 전등이 켜졌습니다.\")\n",
49+
" fun off() = println(\"$location 전등이 꺼졌습니다.\")\n",
50+
"}\n",
51+
"\n",
52+
"class Stereo(val location: String) {\n",
53+
" fun on() = println(\"$location 오디오가 켜졌습니다.\")\n",
54+
" fun setVolume(level: Int) = println(\"$location 오디오 볼륨이 $level 로 설정되었습니다.\")\n",
55+
" fun off() = println(\"$location 오디오가 꺼졌습니다.\")\n",
56+
"}"
57+
],
58+
"id": "eac866c2f50aa4c0",
59+
"outputs": [],
60+
"execution_count": 9
61+
},
62+
{
63+
"metadata": {
64+
"executionRelatedData": {
65+
"compiledClasses": [
66+
"Line_7_jupyter",
67+
"Line_12_jupyter"
68+
]
69+
},
70+
"ExecuteTime": {
71+
"end_time": "2026-02-07T09:18:03.621374Z",
72+
"start_time": "2026-02-07T09:18:03.533189Z"
73+
}
74+
},
75+
"cell_type": "code",
76+
"outputs": [],
77+
"execution_count": 10,
78+
"source": [
79+
"// Command (명령 객체)\n",
80+
"interface Command {\n",
81+
" fun execute()\n",
82+
" fun undo()\n",
83+
"}\n",
84+
"\n",
85+
"// 전등 켜기 명령\n",
86+
"class LightOnCommand(private val light: Light) : Command {\n",
87+
" override fun execute() = light.on()\n",
88+
" override fun undo() = light.off()\n",
89+
"}\n",
90+
"\n",
91+
"// 오디오 켜기 명령\n",
92+
"class StereoOnWithCDCommand(private val stereo: Stereo) : Command {\n",
93+
" override fun execute() {\n",
94+
" stereo.on()\n",
95+
" stereo.setVolume(11)\n",
96+
" }\n",
97+
" override fun undo() = stereo.off()\n",
98+
"}"
99+
],
100+
"id": "103fdf4aa8543248"
101+
},
102+
{
103+
"metadata": {
104+
"ExecuteTime": {
105+
"end_time": "2026-02-07T09:18:11.412345Z",
106+
"start_time": "2026-02-07T09:18:11.311184Z"
107+
},
108+
"executionRelatedData": {
109+
"compiledClasses": [
110+
"Line_14_jupyter"
111+
]
112+
}
113+
},
114+
"cell_type": "code",
115+
"outputs": [],
116+
"execution_count": 12,
117+
"source": [
118+
"/* Invoker (호출자) : 명령이 무엇인지 모름. 그저 execute()를 호출할 슬롯을 가진 리모컨 */\n",
119+
"\n",
120+
"class RemoteControl {\n",
121+
" private var slot: Command? = null\n",
122+
" private var lastCommand: Command? = null // Undo를 위한 기록\n",
123+
"\n",
124+
" fun setCommand(command: Command) {\n",
125+
" slot = command\n",
126+
" }\n",
127+
"\n",
128+
" fun buttonWasPressed() {\n",
129+
" slot?.execute()\n",
130+
" lastCommand = slot\n",
131+
" }\n",
132+
"\n",
133+
" fun undoButtonWasPressed() {\n",
134+
" print(\"Undo 실행: \")\n",
135+
" lastCommand?.undo()\n",
136+
" }\n",
137+
"}"
138+
],
139+
"id": "afed5c265c371de6"
140+
},
141+
{
142+
"metadata": {},
143+
"cell_type": "markdown",
144+
"source": [
145+
"**널 커맨드 (Null Object Pattern)**\n",
146+
"보통 리모컨의 버튼이 비어있으면 `if (slot != null)` 같은 체크가 필요. 하지만 아무 일도 하지 않는 `NoCommand` 객체를 넣어두면, 조건문 없이도 안전하게 실행할 수 있음."
147+
],
148+
"id": "ea7d081cd94d3c5c"
149+
},
150+
{
151+
"metadata": {
152+
"ExecuteTime": {
153+
"end_time": "2026-02-07T09:24:34.102264Z",
154+
"start_time": "2026-02-07T09:24:33.992049Z"
155+
},
156+
"executionRelatedData": {
157+
"compiledClasses": [
158+
"Line_17_jupyter"
159+
]
160+
}
161+
},
162+
"cell_type": "code",
163+
"outputs": [],
164+
"execution_count": 15,
165+
"source": [
166+
"class NoCommand : Command {\n",
167+
" override fun execute() { /* 아무것도 하지 않음 */ }\n",
168+
" override fun undo() { /* 아무것도 하지 않음 */ }\n",
169+
"}\n",
170+
"\n",
171+
"// Invoker 수정 (초기값을 NoCommand로 설정)\n",
172+
"class RemoteControlWithNoCommand {\n",
173+
" private var slot: Command = NoCommand() // null 대신 NoCommand 사용\n",
174+
"\n",
175+
" fun setCommand(command: Command) {\n",
176+
" slot = command\n",
177+
" }\n",
178+
"\n",
179+
" fun buttonWasPressed() {\n",
180+
" slot.execute() // null 체크(?.) 없이 바로 호출 가능!\n",
181+
" }\n",
182+
"}"
183+
],
184+
"id": "83fef229e39c319c"
185+
},
186+
{
187+
"metadata": {
188+
"ExecuteTime": {
189+
"end_time": "2026-02-07T09:19:23.348934Z",
190+
"start_time": "2026-02-07T09:19:23.177325Z"
191+
},
192+
"executionRelatedData": {
193+
"compiledClasses": [
194+
"Line_16_jupyter"
195+
]
196+
}
197+
},
198+
"cell_type": "code",
199+
"outputs": [
200+
{
201+
"name": "stdout",
202+
"output_type": "stream",
203+
"text": [
204+
"거실 전등이 켜졌습니다.\n",
205+
"Undo 실행: 거실 전등이 꺼졌습니다.\n",
206+
"--------------------\n",
207+
"주방 오디오가 켜졌습니다.\n",
208+
"주방 오디오 볼륨이 11 로 설정되었습니다.\n",
209+
"Undo 실행: 주방 오디오가 꺼졌습니다.\n"
210+
]
211+
}
212+
],
213+
"execution_count": 14,
214+
"source": [
215+
"// ~ 실습 ~\n",
216+
"\n",
217+
"// 1. 리시버 생성\n",
218+
"val livingRoomLight = Light(\"거실\")\n",
219+
"val kitchenStereo = Stereo(\"주방\")\n",
220+
"\n",
221+
"// 2. 커맨드 생성\n",
222+
"val lightOn = LightOnCommand(livingRoomLight)\n",
223+
"val stereoOn = StereoOnWithCDCommand(kitchenStereo)\n",
224+
"\n",
225+
"// 3. 인보커(리모컨) 준비\n",
226+
"val remote = RemoteControl()\n",
227+
"\n",
228+
"// 4. 테스트: 전등 켜기\n",
229+
"remote.setCommand(lightOn)\n",
230+
"remote.buttonWasPressed()\n",
231+
"remote.undoButtonWasPressed()\n",
232+
"\n",
233+
"println(\"--------------------\")\n",
234+
"\n",
235+
"// 5. 테스트: 오디오 켜기\n",
236+
"remote.setCommand(stereoOn)\n",
237+
"remote.buttonWasPressed()\n",
238+
"remote.undoButtonWasPressed()"
239+
],
240+
"id": "cf762b5ffab61b4"
241+
},
242+
{
243+
"metadata": {},
244+
"cell_type": "markdown",
245+
"source": [
246+
"**매크로 커맨드(Macro Command)**\n",
247+
"여러 개의 명령을 리스트로 묶어 버튼 하나로 동시에 실행하는 기능.\n",
248+
"'파티 모드'나 '취침 모드' 같은 기능을 만들 때 유용."
249+
],
250+
"id": "44ed4c4f6ebba6c"
251+
},
252+
{
253+
"metadata": {},
254+
"cell_type": "code",
255+
"outputs": [],
256+
"execution_count": null,
257+
"source": [
258+
"class MacroCommand(private val commands: List<Command>) : Command {\n",
259+
" override fun execute() {\n",
260+
" commands.forEach { it.execute() }\n",
261+
" }\n",
262+
"\n",
263+
" override fun undo() {\n",
264+
" // 취소는 실행의 역순으로 해야 안전\n",
265+
" commands.reversed().forEach { it.undo() }\n",
266+
" }\n",
267+
"}\n",
268+
"\n",
269+
"// 사용 예시\n",
270+
"val partyMode = MacroCommand(listOf(lightOn, stereoOn))\n",
271+
"remote.setCommand(partyMode)\n",
272+
"remote.buttonWasPressed()"
273+
],
274+
"id": "754fc6571262ffc"
275+
},
276+
{
277+
"metadata": {},
278+
"cell_type": "markdown",
279+
"source": [
280+
"## 3. 장점\n",
281+
"- DIP (의존 역전 원칙): 고수준 모듈(Invoker)이 저수준 모듈(Receiver)의 구체적인 구현에 의존하지 않게 됩니다.\n",
282+
"- 확장성: 새로운 커맨드 클래스를 추가해도 기존 코드를 수정할 필요가 없습니다.\n",
283+
"- 작업 취소(Undo): execute()의 반대 연산을 수행하는 undo() 메서드를 구현하면 쉽게 이전 상태로 되돌릴 수 있습니다.\n",
284+
"- 매크로 커맨드: 여러 명령을 리스트로 담아 한 번에 실행할 수 있습니다. (예: '취침 모드' 버튼 하나로 전등 끄기 + 커튼 닫기)"
285+
],
286+
"id": "3efcc2c7db3a0008"
287+
}
288+
],
289+
"metadata": {
290+
"kernelspec": {
291+
"display_name": "Kotlin",
292+
"language": "kotlin",
293+
"name": "kotlin"
294+
},
295+
"language_info": {
296+
"name": "kotlin",
297+
"version": "2.2.20",
298+
"mimetype": "text/x-kotlin",
299+
"file_extension": ".kt",
300+
"pygments_lexer": "kotlin",
301+
"codemirror_mode": "text/x-kotlin",
302+
"nbconvert_exporter": ""
303+
}
304+
},
305+
"nbformat": 4,
306+
"nbformat_minor": 5
307+
}

ch06/img.png

1.08 MB
Loading

ch06/img_1.png

1.29 MB
Loading

ch06/img_2.png

34 KB
Loading

ch06/img_3.png

33.8 KB
Loading

ch06/img_4.png

69.9 KB
Loading

ch06/img_5.png

86.6 KB
Loading

0 commit comments

Comments
 (0)