271. 커맨드 (Command) - 요청을 객체로 캡슐화 (Undo/Redo 지원)

핵심 인사이트 (3줄 요약)

  1. 본질: 커맨드(Command) 패턴은 실행될 기능(요청)을 독립적인 '객체'로 캡슐화하여, 요청을 하는 쪽(Invoker)과 요청을 수행하는 쪽(Receiver) 간의 의존성을 완벽하게 분리하는 행동(Behavioral) 패턴이다.
  2. 가치: 단순히 메서드를 호출하는 것을 넘어 "호출 자체를 데이터화(객체화)"하기 때문에, 명령의 큐(Queue) 저장, 로깅, 스케줄링, 그리고 가장 강력한 기능인 **실행 취소(Undo) 및 재실행(Redo)**을 아키텍처 수준에서 구현할 수 있게 해준다.
  3. 융합: 단일 버튼이 여러 기능을 동적으로 수행해야 하는 GUI 애플리케이션(예: 단축키, 매크로)뿐만 아니라, 엔터프라이즈 환경에서의 비동기 작업 큐, 트랜잭션 보상(Saga Pattern) 로직 등 분산 시스템의 명령 제어 모델로 널리 융합된다.

Ⅰ. 개요 및 필요성 (Context & Necessity)

  • 개념: 커맨드 패턴은 요청 내역(어떤 객체의 어떤 메서드를 어떤 매개변수로 호출할지)을 캡슐화한 Command 객체를 만들고, 이를 실행 버튼(Invoker)에 할당하여 실행시키는 설계 기법이다.

  • 필요성: 만능 스마트 리모컨을 만든다고 가정하자. 1번 버튼은 TV를 켜고, 2번 버튼은 전등을 꺼야 한다. 리모컨 내부 버튼 코드에 TV.turnOn()이나 Light.turnOff()를 직접(하드코딩) 작성하면, 나중에 1번 버튼으로 에어컨을 켜고 싶을 때 리모컨 코드를 뜯어고쳐야 한다. 리모컨(버튼)은 자신이 누굴 제어하는지 모른 채 "나한테 할당된 무언가를 실행(execute())한다"는 행위만 하도록 완전히 분리해야 한다.

  • 💡 비유: 식당에서 손님이 웨이터에게 주문을 하는 과정과 같습니다. 손님이 요리사에게 직접 "스테이크 구워주세요"라고 말하지 않습니다. 손님은 웨이터에게 말하고, 웨이터는 그것을 **'주문서(Command 객체)'**라는 종이에 적어 주방에 전달합니다. 웨이터는 요리법을 몰라도 그저 "주문 들어왔다(execute())"라고만 외치면 됩니다. 주문서 덕분에 취소(Undo)나 예약(Queueing)도 가능해집니다.

  • 등장 배경 및 발전 과정:

    1. 강결합에 의한 재사용성 저하: UI 버튼과 비즈니스 로직이 엉겨 있어, '복사' 기능 버튼과 '복사' 단축키(Ctrl+C), '복사' 메뉴를 각각 따로 짜야 했다.
    2. 명령의 객체화 (Reification): '복사하다'라는 행위 자체를 CopyCommand라는 객체로 만들어, 버튼이든 단축키든 이 객체 하나만 바라보게(호출하게) 만들었다.
    3. 상태 저장 메커니즘의 결합: 명령이 수행될 때 이전 상태를 객체 내부에 저장해 두면 역방향 실행(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번 테이블 스테이크를 구워라!"라는 '주문서' 그 자체입니다.


Ⅳ. 실무 적용 및 기술사적 판단

실무 시나리오

  1. 시나리오 — 포토샵/워드프로세서의 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)이다.
  2. 시나리오 — 대용량 비동기 작업 큐 (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 연결, 파일 파싱, 계산 등 수천 줄의 로직을 직접 짜는 행위. 커맨드는 본질적으로 InvokerReceiver 사이의 '전달자'일 뿐이다. 실제 복잡한 로직은 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줄 비유 설명

  1. 여러분이 식당에서 "치즈버거 주세요"라고 말하면(행위), 종업원이 그걸 **'주문서'**라는 종이(객체)에 쓱싹 적어요.
  2. 주문서라는 '물건'이 되었기 때문에, 종업원은 주문서를 주방에 킵(Queue)해둘 수도 있고, 여러분이 "아차! 취소요!" 하면 그 종이를 버려서 실행을 무를(Undo) 수도 있죠.
  3. 이렇게 어떤 행동(명령)을 눈에 보이지 않는 공기 속으로 날려버리는 게 아니라, 손에 쥘 수 있는 딱지(객체)로 만들어 마음대로 조종하는 것을 **'커맨드 패턴'**이라고 부른답니다!