271. 커맨드 (Command) - 요청을 객체로 캡슐화 (Undo/Redo 지원)
핵심 인사이트 (3줄 요약)
- 본질: 커맨드(Command) 패턴은 실행될 기능(요청)을 독립적인 '객체'로 캡슐화하여, 요청을 하는 쪽(Invoker)과 요청을 수행하는 쪽(Receiver) 간의 의존성을 완벽하게 분리하는 행동(Behavioral) 패턴이다.
- 가치: 단순히 메서드를 호출하는 것을 넘어 "호출 자체를 데이터화(객체화)"하기 때문에, 명령의 큐(Queue) 저장, 로깅, 스케줄링, 그리고 가장 강력한 기능인 **실행 취소(Undo) 및 재실행(Redo)**을 아키텍처 수준에서 구현할 수 있게 해준다.
- 융합: 단일 버튼이 여러 기능을 동적으로 수행해야 하는 GUI 애플리케이션(예: 단축키, 매크로)뿐만 아니라, 엔터프라이즈 환경에서의 비동기 작업 큐, 트랜잭션 보상(Saga Pattern) 로직 등 분산 시스템의 명령 제어 모델로 널리 융합된다.
Ⅰ. 개요 및 필요성 (Context & Necessity)
-
개념: 커맨드 패턴은 요청 내역(어떤 객체의 어떤 메서드를 어떤 매개변수로 호출할지)을 캡슐화한
Command객체를 만들고, 이를 실행 버튼(Invoker)에 할당하여 실행시키는 설계 기법이다. -
필요성: 만능 스마트 리모컨을 만든다고 가정하자. 1번 버튼은 TV를 켜고, 2번 버튼은 전등을 꺼야 한다. 리모컨 내부 버튼 코드에
TV.turnOn()이나Light.turnOff()를 직접(하드코딩) 작성하면, 나중에 1번 버튼으로 에어컨을 켜고 싶을 때 리모컨 코드를 뜯어고쳐야 한다. 리모컨(버튼)은 자신이 누굴 제어하는지 모른 채 "나한테 할당된 무언가를 실행(execute())한다"는 행위만 하도록 완전히 분리해야 한다. -
💡 비유: 식당에서 손님이 웨이터에게 주문을 하는 과정과 같습니다. 손님이 요리사에게 직접 "스테이크 구워주세요"라고 말하지 않습니다. 손님은 웨이터에게 말하고, 웨이터는 그것을 **'주문서(Command 객체)'**라는 종이에 적어 주방에 전달합니다. 웨이터는 요리법을 몰라도 그저 "주문 들어왔다(
execute())"라고만 외치면 됩니다. 주문서 덕분에 취소(Undo)나 예약(Queueing)도 가능해집니다. -
등장 배경 및 발전 과정:
- 강결합에 의한 재사용성 저하: UI 버튼과 비즈니스 로직이 엉겨 있어, '복사' 기능 버튼과 '복사' 단축키(Ctrl+C), '복사' 메뉴를 각각 따로 짜야 했다.
- 명령의 객체화 (Reification): '복사하다'라는 행위 자체를
CopyCommand라는 객체로 만들어, 버튼이든 단축키든 이 객체 하나만 바라보게(호출하게) 만들었다. - 상태 저장 메커니즘의 결합: 명령이 수행될 때 이전 상태를 객체 내부에 저장해 두면 역방향 실행(
undo())이 가능해짐을 깨닫고, 텍스트 에디터나 그래픽 툴의 필수 패턴으로 정착했다.
-
📢 섹션 요약 비유: 행동(명령)을 빈 캡슐(캡슐 장난감) 안에 쏙 집어넣어, 언제든 뽑아서(Queue) 누르고(Execute), 잘못 누르면 다시 주워 담는(Undo) 기술입니다.
Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)
구성 요소 (클래스 다이어그램)
| 요소명 | 역할 | 비유 |
|---|---|---|
| Command (명령 인터페이스) | 모든 명령 객체가 공유하는 인터페이스. 보통 execute()와 undo() 메서드만 가진다. | 표준 규격의 주문서 양식 |
| ConcreteCommand | 특정 동작과 Receiver를 연결(바인딩)하는 구체적인 클래스. execute()가 호출되면 Receiver의 실제 메서드를 실행한다. | "1번 테이블 스테이크 주문" (실제 요리사 정보 포함) |
| Receiver (수신자) | 명령이 수행될 때 실제로 비즈니스 로직을 처리하는 객체. (TV, 조명, 문서 등) | 주방의 요리사 (실제 스테이크를 굽는 사람) |
| Invoker (호출자) | Command 객체를 전달받아 보관하고 있다가, 특정 시점(예: 클릭)에 Command.execute()를 호출하는 객체. | 주문서를 받아들고 주방에 외치는 웨이터 (또는 리모컨 버튼) |
| Client (클라이언트) | Receiver를 생성하고, 이를 조작할 ConcreteCommand를 만들어 Invoker에 조립(세팅)하는 주체. | 손님 (무엇을 먹을지 결정하여 주문을 넣는 주체) |
동작 메커니즘 (코드 뼈대 구조)
커맨드 패턴의 핵심은 Invoker(리모컨)가 Receiver(TV)를 전혀 모르도록, 그 사이에 Command라는 완충재를 끼워 넣는 것이다.
┌─────────────────────────────────────────────────────────────┐
│ 커맨드 패턴의 실행 흐름 (Decoupling) │
├─────────────────────────────────────────────────────────────┤
│ │
│ [Client] (세팅 주체) │
│ 1. Light receiver = new Light(); │
│ 2. Command c = new LightOnCommand(receiver); │
│ 3. RemoteControl invoker = new RemoteControl(); │
│ 4. invoker.setCommand(c); │
│ │
│ ───(세팅 완료 후, 사용자가 버튼을 누르면)──────────────────────── │
│ │
│ [Invoker (리모컨)] [Command (인터페이스)] │
│ + buttonPressed() { + execute() │
│ command.execute(); ───▶ △ │
│ } │ (구현) │
│ ┌─────┴───────┐ │
│ │ LightOnCmd │ │
│ │ - receiver │ │
│ │ + execute() │ │
│ └─────┬───────┘ │
│ │ │
│ │ (receiver.turnOn() 호출)│
│ ▼ │
│ [Receiver (조명)] │
│ + turnOn() { "불이 켜짐" } │
└─────────────────────────────────────────────────────────────┘
[다이어그램 해설] RemoteControl(Invoker)의 코드 내부에는 Light.turnOn()이라는 단어가 단 한 글자도 없다. 오직 command.execute()만 있을 뿐이다. 클라이언트가 실행 시간(Runtime)에 어떤 구체적 커맨드 객체를 주입하느냐에 따라 리모컨 버튼의 기능이 무한히 바뀔 수 있다. 또한 여러 커맨드 객체를 배열(리스트)로 묶어서 execute()를 연속 호출하면 '매크로(Macro)' 기능이 단숨에 구현된다.
매직 (Magic): Undo(실행 취소) 원리
커맨드 패턴이 다른 위임 패턴과 구별되는 가장 강력한 무기는 undo()의 지원이다. 구체적인 커맨드 객체는 execute()를 수행하기 직전의 상태(예: 에디터의 변경 전 텍스트, 이동 전 좌표)를 자신의 내부에 멤버 변수로 저장(백업)해 둔다.
-
사용자가 실행 취소(Ctrl+Z)를 누르면,
Invoker는 저장해 둔(Stack) 가장 최근의Command객체를 꺼내어undo()메서드를 호출한다. -
undo()메서드는 저장해 두었던 이전 상태를Receiver에게 다시 덮어씌워(복원시켜) 행동을 되돌린다. -
📢 섹션 요약 비유: 로봇 장난감에게 "앞으로 한 칸 가라"는 명령서(Command)를 줄 때, 명령서 뒷면에 몰래 "원래는 뒤에 있었음"이라고 적어두는 것과 같습니다. 취소 버튼을 누르면 뒷면을 읽고 로봇을 원래 위치로 되돌리는 완벽한 트릭입니다.
Ⅲ. 융합 비교 및 다각도 분석
1. 커맨드(Command) vs 전략(Strategy) vs 상태(State)
이 세 패턴 모두 '인터페이스를 통해 행위를 캡슐화'한다는 점에서 구조적으로 유사(클래스 다이어그램이 거의 비슷함)하지만, **무엇을 캡슐화하는가(Intent)**가 전혀 다르다.
| 비교 항목 | 커맨드 (Command) | 전략 (Strategy) | 상태 (State) |
|---|---|---|---|
| 캡슐화의 대상 | '요청(명령) 자체' (예: 복사하기, 불 켜기) | '어떻게 할 것인가(알고리즘)' (예: 정렬 방식) | '어떤 상태인가(내부 상태)' (예: 자판기 품절) |
| 결과물의 성격 | 명령은 실행 후 버려지거나 스택에 쌓임 | 하나의 목적을 위해 교체되는 부품 | 조건에 따라 스스로 폼을 바꾸는 생명체 |
| 동작의 횟수 | 주로 1회성 실행(호출)을 목적으로 함 | 지속적인 알고리즘 제공 | 지속적인 룰 제공 |
| 주요 사용처 | Undo/Redo, 작업 큐(Queue), 매크로 | 결제 방식, 압축 알고리즘, 경로 탐색 | TCP 연결 상태, 게임 캐릭터의 상태 |
과목 융합 관점
-
운영체제 (OS): 스케줄러(Scheduler)의 작업 대기열(Ready Queue)에 들어가는 스레드나 프로세스 컨트롤 블록(PCB)의 실행 정보가 커맨드 패턴의 개념적 확장이다. 각 작업(커맨드)을 큐에 밀어 넣고(En-queue) 워커 스레드가 이를 빼내어 실행(Execute)하는 것이 스레드 풀(Thread Pool)의 핵심 원리다.
-
클라우드 / MSA: 마이크로서비스 환경에서 트랜잭션을 일관되게 유지하기 위한 사가(Saga) 패턴은, 각 서비스의 트랜잭션 성공 시
execute()를 수행하고, 도중에 실패하면 이전에 성공한 서비스들의undo()(보상 트랜잭션)를 역순으로 호출하는 분산 커맨드 아키텍처다. -
📢 섹션 요약 비유: 전략 패턴이 "스테이크를 '어떻게' 구울지(레어, 웰던) 결정하는 요리법"이라면, 커맨드 패턴은 "지금 당장 3번 테이블 스테이크를 구워라!"라는 '주문서' 그 자체입니다.
Ⅳ. 실무 적용 및 기술사적 판단
실무 시나리오
-
시나리오 — 포토샵/워드프로세서의 Undo/Redo (히스토리 기능): 텍스트 에디터를 개발 중이다. 사용자의 타이핑, 지우기, 폰트 색상 변경 등 모든 액션을 Ctrl+Z로 무한정 취소할 수 있어야 하고, Ctrl+Y로 다시 되돌릴 수 있어야 한다.
- 아키텍트의 해결책: 모든 편집 행위를
Command인터페이스(execute(),undo())의 구현체로 만든다.InsertTextCommand,ChangeColorCommand등을 만들고, 사용자가 행위를 할 때마다 이를Stack<Command> undoStack에 쌓는다(Push). Ctrl+Z를 누르면undoStack에서 꺼내어undo()를 실행한 뒤, 그 명령 객체를Stack<Command> redoStack으로 옮긴다. 이 쌍둥이 스택 구조가 세상 모든 Undo/Redo 기능의 업계 표준(De facto)이다.
- 아키텍트의 해결책: 모든 편집 행위를
-
시나리오 — 대용량 비동기 작업 큐 (Job Queue) 및 스케줄링: 대규모 이메일 발송, 이미지 리사이징 등 무거운 비즈니스 로직을 API 응답 쓰레드에서 직접 처리하면 서버가 다운된다. 비동기 백그라운드 처리가 필요하다.
- 아키텍트의 해결책:
EmailSendCommand,ImageResizeCommand객체를 생성하여 직렬화(JSON 등)한 뒤 Message Queue(RabbitMQ, Kafka, Redis)에 던져 넣는다. 뒷단의 워커(Worker) 서버들은 이 큐에서 커맨드 데이터를 꺼내 역직렬화한 뒤execute()만 무한히 실행한다. 커맨드 패턴이 메모리 밖으로 확장되어 분산 비동기 큐의 핵심 도구로 승화된 것이다.
- 아키텍트의 해결책:
도입 체크리스트
- 기술적:
undo()를 구현할 때 롤백(Rollback) 데이터를 객체 안에 너무 많이 저장하면(예: 포토샵에서 1GB짜리 이미지 전체 복사본을 명령 10개마다 저장), 램(RAM)이 금방 터진다(OOM). 차이점(Diff)만 저장하거나 한도(Max History)를 설정했는가? - 설계적: 버튼마다 Command 클래스를 만들면 '클래스 폭발'이 일어난다. 단순한 호출이라면 람다(Lambda) 함수나 일급 객체(First-class Citizen) 함수 포인터로 가볍게 넘기는 방식(경량 커맨드 패턴)을 쓸 수 있는지 현대적 언어 스펙을 고려했는가?
안티패턴
-
명령 객체 내부의 비대한 비즈니스 로직 (God Command):
Command객체의execute()안에 DB 연결, 파일 파싱, 계산 등 수천 줄의 로직을 직접 짜는 행위. 커맨드는 본질적으로Invoker와Receiver사이의 '전달자'일 뿐이다. 실제 복잡한 로직은Receiver가 갖고, 커맨드는 단순히receiver.doHeavyWork()를 호출하는 '얄팍한(Thin)' 중계자여야 재사용성이 높다. -
📢 섹션 요약 비유: 택배 배송장(Command)에 물건을 보내는 주소와 방법만 간단히 적혀있어야지, 배송장 안에 실제 물건(수천 줄의 로직)까지 구겨 넣으면 무거워서 배달(유지보수)이 불가능해집니다.
Ⅴ. 기대효과 및 결론
정량/정성 기대효과
| 구분 | 강결합 호출 방식 (AS-IS) | 커맨드 패턴 도입 (TO-BE) | 개선 효과 |
|---|---|---|---|
| 정량 | Undo/Redo 기능 구현 불가 (새로 짜야 함) | History Stack 적용으로 즉각 지원 | Undo/Redo 개발 및 유지보수 비용 거의 0에 수렴 |
| 정량 | 버튼 수만큼 UI와 비즈니스 로직 연동 코드 발생 | Command 객체 1개로 UI 10개 동시 연결 | 중복 로직 제거로 코드량 대폭 절감 |
| 정성 | 작업을 나중에 처리하거나 로그에 남기기 힘듦 | 명령이 '객체(데이터)'이므로 DB 저장/로깅 용이 | 감사(Audit) 로그 및 비동기 시스템 아키텍처 기반 확보 |
미래 전망
- 이벤트 소싱(Event Sourcing)과 CQRS로의 확장: MSA의 궁극기인 이벤트 소싱은 "DB에 최종 상태를 덮어쓰는 대신, 발생한 모든 상태 변경 '명령(커맨드/이벤트)'을 순서대로 블록체인처럼 로깅해 두는 방식"이다. 과거로 돌리려면 로그(명령)를 거꾸로
undo하거나, 처음부터 다시execute하여 현재 상태 복원을 한다. 커맨드 패턴이 분산 DB의 데이터 저장 철학 자체를 바꿔놓은 것이다. - 함수형 프로그래밍에 의한 해체: Java의
Runnable,Consumer인터페이스나 화살표 함수(() => receiver.action())의 등장으로, 무거운ConcreteCommand클래스를 수십 개씩 정의하던 과거의 GoF 시절 커맨드 패턴은 한 줄짜리 함수 전달 방식으로 해체(흡수)되고 있다.
참고 표준
- GoF (Gang of Four): Behavioral Patterns - Command
- Java API:
java.lang.Runnable,javax.swing.Action - Spring Framework:
JdbcTemplate이나 트랜잭션 관리 내부의 Callback 메커니즘
커맨드 패턴은 **"행위(Verb)를 명사(Noun)로 취급한다"**는 역발상을 통해 태어난 천재적인 패턴이다. 행위를 객체로 만들었기에 우리는 그것을 변수에 담고, 매개변수로 던지고, 파일로 저장하고, 네트워크 너머로 날려 보낼 수 있게 되었다. 기술사는 이 패턴이 단순한 GUI 버튼 클릭을 넘어, 현대 비동기 분산 메시지 큐와 이벤트 소싱을 떠받치는 가장 거대하고 본질적인 아키텍처 뼈대임을 꿰뚫어 보아야 한다.
- 📢 섹션 요약 비유: 머릿속의 '생각(행위)'을 '편지(객체)'로 써서 봉투에 담아내는 순간, 우리는 그 생각을 서랍에 보관(저장)할 수도 있고, 우체통에 넣어 나중에 보내게(스케줄링) 할 수도 있으며, 심지어 취소해 달라고 회수(Undo)할 수도 있게 된 것과 같은 놀라운 혁명입니다.
📌 관련 개념 맵 (Knowledge Graph)
| 개념 명칭 | 관계 및 시너지 설명 |
|---|---|
| 메멘토 패턴 (Memento Pattern) | 커맨드 객체가 undo()를 수행할 때 이전 상태를 안전하게 저장(백업)하고 복원하기 위해 종종 짝꿍으로 함께 쓰이는 패턴. |
| 전략 패턴 (Strategy Pattern) | 커맨드가 '어떤 일을 할 것인가(What)'를 객체화했다면, 전략은 '그 일을 어떻게 할 것인가(How)'를 객체화한 경쟁적 패턴. |
| Saga 패턴 (분산 트랜잭션) | 마이크로서비스에서 여러 커맨드를 체이닝하고, 실패 시 역방향으로 보상 커맨드(Undo)를 실행하여 데이터 정합성을 맞추는 아키텍처. |
| 메시지 큐 (Message Queue, MQ) | 커맨드 객체를 직렬화하여 쌓아두는 인프라스트럭처로, 커맨드 패턴을 비동기/분산 환경으로 확장시키는 런타임 저장소. |
| 이벤트 소싱 (Event Sourcing) | 데이터의 최종 결과값을 저장하는 대신, 데이터를 변경한 '커맨드(이벤트)'들의 기록 전체를 DB에 저장하는 첨단 데이터 설계 방식. |
👶 어린이를 위한 3줄 비유 설명
- 여러분이 식당에서 "치즈버거 주세요"라고 말하면(행위), 종업원이 그걸 **'주문서'**라는 종이(객체)에 쓱싹 적어요.
- 주문서라는 '물건'이 되었기 때문에, 종업원은 주문서를 주방에 킵(Queue)해둘 수도 있고, 여러분이 "아차! 취소요!" 하면 그 종이를 버려서 실행을 무를(Undo) 수도 있죠.
- 이렇게 어떤 행동(명령)을 눈에 보이지 않는 공기 속으로 날려버리는 게 아니라, 손에 쥘 수 있는 딱지(객체)로 만들어 마음대로 조종하는 것을 **'커맨드 패턴'**이라고 부른답니다!