|
| 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 | + "\n", |
| 14 | + "- 손님 (Client): 주문을 결정하고 웨이트리스에게 전달합.\n", |
| 15 | + "- 주문서 (Command): 주문 내용이 적힌 객체. 주방장에게 무엇을 할지 알려주는 인터페이스 역할.\n", |
| 16 | + "- 웨이트리스 (Invoker): 주문서를 받아서 주방장에게 전달 (주방장이 구체적으로 뭘 하는지는 몰라도 됨.)\n", |
| 17 | + "- 주방장 (Receiver): 실제로 요리를 하는 객체.\n", |
| 18 | + "\n", |
| 19 | + "## 2. 클래스 구성\n", |
| 20 | + "\n", |
| 21 | + "\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 | +} |
0 commit comments