Command
Command는 요청을 요청에 대한 모든 정보를 포함하는 독립 객체로 변환하는 행동 디자인 패턴입니다. 이 변환을 통해 요청을 메서드 인수로 전달하고, 요청 실행을 지연하거나 큐에 넣고, 실행 취소 가능한 작업을 지원할 수 있습니다.

Intent
Command 패턴은 특정 메서드 호출을 독립 객체로 변환하여 다양한 이점을 제공합니다:
- 요청을 메서드 인수로 전달
- 요청 실행 지연 또는 큐잉
- 실행 취소(Undo) 작업 지원
- 작업 로깅 및 원격 실행
Problem
텍스트 에디터 앱을 개발하고 있다고 가정합시다. 다양한 작업을 위한 버튼이 있는 도구 모음을 만들어야 합니다.

도구 모음의 버튼에 사용할 수 있는 Button 클래스를 만들었습니다. 모든 버튼이 비슷해 보이지만 각각 다른 작업을 수행해야 합니다.

가장 간단한 해결책은 각 버튼 유형에 대해 수많은 서브클래스를 만드는 것입니다. 하지만 이 접근 방식에는 문제가 있습니다:
- 엄청난 수의 서브클래스
- 기본
Button클래스 수정 시 서브클래스 깨짐 위험

더 나쁜 문제가 있습니다. 복사/붙여넣기 같은 작업은 여러 곳에서 호출됩니다:
- 도구 모음의 “복사” 버튼
- 컨텍스트 메뉴
- 키보드 단축키
Ctrl+C
이로 인해 코드가 중복되거나 GUI가 비즈니스 로직에 의존하게 됩니다.
Solution
좋은 소프트웨어 설계는 종종 관심사의 분리 원칙을 기반으로 합니다. GUI 레이어와 비즈니스 로직 레이어를 분리해야 합니다.

Command 패턴은 GUI 객체가 비즈니스 로직 객체에 직접 요청을 보내는 대신, 호출할 객체, 메서드 이름, 인수 목록 등 모든 요청 세부 정보를 명령 클래스로 추출하도록 제안합니다.

명령 객체는 다양한 GUI와 비즈니스 로직 객체 사이의 링크 역할을 합니다. GUI 객체는 명령을 트리거하기만 하면 되고, 명령이 모든 세부 사항을 처리합니다.

다음 단계는 모든 명령이 동일한 인터페이스를 구현하도록 하는 것입니다. 일반적으로 매개변수 없는 단일 실행 메서드만 있습니다. 이를 통해 구체적인 명령 클래스에 결합하지 않고 동일한 요청 발신자로 다양한 명령을 사용할 수 있습니다.
Real-World Analogy

도시를 오래 걸은 후 좋은 레스토랑에 들어가 창가 테이블에 앉습니다. 친절한 웨이터가 다가와 주문을 받아 종이에 적습니다.
웨이터는 주방으로 가서 주문을 벽에 붙입니다. 잠시 후 주문이 요리사에게 전달되고, 요리사가 읽고 요리를 준비합니다.
종이 주문은 명령 역할을 합니다:
- 요리가 준비될 때까지 큐에 대기
- 즉시 또는 예약 실행 가능
- 요리에 필요한 모든 관련 정보 포함
Structure


| 구성 요소 | 역할 |
|---|---|
| Sender (Invoker) | 요청을 시작하는 역할. 명령 객체에 대한 참조를 저장하고 트리거 |
| Command | 명령 실행을 위한 인터페이스 선언. 보통 단일 execute() 메서드 |
| Concrete Commands | 다양한 종류의 요청 구현. 직접 작업하지 않고 비즈니스 로직 객체에 위임 |
| Receiver | 비즈니스 로직을 포함. 대부분의 명령은 실제 작업을 수신자에게 위임 |
| Client | 구체적인 명령 객체를 생성하고 구성 |
Pseudocode
텍스트 에디터의 실행 취소 기능을 구현하는 예시:

// 명령 인터페이스interface Command { execute(): boolean; undo(): void;}
// 추상 명령 클래스 (공통 기능 포함)abstract class EditorCommand implements Command { protected app: Application; protected editor: Editor; protected backup: string = "";
constructor(app: Application, editor: Editor) { this.app = app; this.editor = editor; }
// 에디터 상태 백업 saveBackup(): void { this.backup = this.editor.text; }
// 상태 복원 undo(): void { this.editor.text = this.backup; }
abstract execute(): boolean;}
// 복사 명령 - 상태를 변경하지 않으므로 히스토리에 저장하지 않음class CopyCommand extends EditorCommand { execute(): boolean { this.app.clipboard = this.editor.getSelection(); return false; // 히스토리에 저장하지 않음 }}
// 잘라내기 명령 - 상태를 변경하므로 히스토리에 저장class CutCommand extends EditorCommand { execute(): boolean { this.saveBackup(); this.app.clipboard = this.editor.getSelection(); this.editor.deleteSelection(); return true; // 히스토리에 저장 }}
// 붙여넣기 명령class PasteCommand extends EditorCommand { execute(): boolean { this.saveBackup(); this.editor.replaceSelection(this.app.clipboard); return true; }}
// 실행 취소 명령class UndoCommand extends EditorCommand { execute(): boolean { this.app.undo(); return false; }}
// 명령 히스토리 (스택)class CommandHistory { private history: Command[] = [];
push(command: Command): void { this.history.push(command); }
pop(): Command | undefined { return this.history.pop(); }
isEmpty(): boolean { return this.history.length === 0; }}
// 에디터 클래스 (Receiver)class Editor { text: string = "";
getSelection(): string { // 선택된 텍스트 반환 return "selected text"; }
deleteSelection(): void { // 선택된 텍스트 삭제 }
replaceSelection(text: string): void { // 선택 영역을 주어진 텍스트로 교체 }}
// 애플리케이션 클래스 (Sender/Invoker)class Application { clipboard: string = ""; editors: Editor[] = []; activeEditor: Editor; history: CommandHistory = new CommandHistory();
constructor() { this.activeEditor = new Editor(); this.editors.push(this.activeEditor); }
// UI 생성 및 명령 바인딩 createUI(): void { // 복사 버튼 const copyButton = document.getElementById("copy"); copyButton?.addEventListener("click", () => { this.executeCommand(new CopyCommand(this, this.activeEditor)); });
// 잘라내기 버튼 const cutButton = document.getElementById("cut"); cutButton?.addEventListener("click", () => { this.executeCommand(new CutCommand(this, this.activeEditor)); });
// 붙여넣기 버튼 const pasteButton = document.getElementById("paste"); pasteButton?.addEventListener("click", () => { this.executeCommand(new PasteCommand(this, this.activeEditor)); });
// 실행 취소 버튼 const undoButton = document.getElementById("undo"); undoButton?.addEventListener("click", () => { this.executeCommand(new UndoCommand(this, this.activeEditor)); });
// 키보드 단축키 document.addEventListener("keydown", (e) => { if (e.ctrlKey && e.key === "c") { this.executeCommand(new CopyCommand(this, this.activeEditor)); } if (e.ctrlKey && e.key === "x") { this.executeCommand(new CutCommand(this, this.activeEditor)); } if (e.ctrlKey && e.key === "v") { this.executeCommand(new PasteCommand(this, this.activeEditor)); } if (e.ctrlKey && e.key === "z") { this.executeCommand(new UndoCommand(this, this.activeEditor)); } }); }
// 명령 실행 executeCommand(command: Command): void { if (command.execute()) { this.history.push(command); } }
// 실행 취소 undo(): void { const command = this.history.pop(); if (command) { command.undo(); } }}Applicability
다음의 경우에 패턴을 사용합니다:
작업으로 객체를 매개변수화할 때
특정 메서드 호출을 독립 객체로 변환하여 인수로 전달하고, 다른 객체에 저장하고, 런타임에 연결된 명령을 전환할 수 있습니다.
작업을 큐에 넣거나 예약할 때
명령을 직렬화하여 파일이나 데이터베이스에 저장하고 나중에 복원하여 실행할 수 있습니다. 이를 통해 지연 실행, 큐잉, 로깅, 네트워크를 통한 원격 실행이 가능합니다.
실행 취소 가능한 작업을 구현할 때
작업을 되돌리려면 수행된 작업의 히스토리를 구현해야 합니다. 명령 히스토리는 실행된 모든 명령 객체와 관련 애플리케이션 상태 백업을 포함하는 스택입니다.
How to Implement
-
Command 인터페이스 선언: 단일 실행 메서드로 명령 인터페이스를 선언합니다.
-
구체적인 명령 추출: 요청을 명령 인터페이스를 구현하는 구체적인 명령 클래스로 추출합니다. 각 클래스에는 요청 인수와 실제 수신자 참조를 저장하는 필드가 있어야 합니다.
-
Sender 식별: 발신자 역할을 할 클래스를 식별합니다. 이 클래스에 명령을 저장할 필드를 추가합니다. 발신자는 명령 인터페이스를 통해서만 명령과 통신해야 합니다.
-
Sender 수정: 발신자가 수신자에게 직접 요청을 보내는 대신 명령을 실행하도록 수정합니다.
-
클라이언트 초기화: 클라이언트는 다음 순서로 객체를 초기화해야 합니다:
- 수신자 생성
- 명령 생성 및 수신자 연결
- 발신자 생성 및 특정 명령 연결
Pros and Cons
장점
| 장점 | 설명 |
|---|---|
| Single Responsibility Principle | 작업을 호출하는 클래스와 수행하는 클래스를 분리할 수 있습니다 |
| Open/Closed Principle | 기존 클라이언트 코드를 수정하지 않고 새 명령을 도입할 수 있습니다 |
| Undo/Redo 구현 | 실행 취소/다시 실행을 구현할 수 있습니다 |
| 지연 실행 | 작업의 지연 실행을 구현할 수 있습니다 |
| 명령 조합 | 간단한 명령들을 조합하여 복잡한 명령을 만들 수 있습니다 |
단점
| 단점 | 설명 |
|---|---|
| 코드 복잡성 | 발신자와 수신자 사이에 완전히 새로운 레이어를 도입하므로 코드가 복잡해질 수 있습니다 |
Relations with Other Patterns
| 패턴 | 관계 |
|---|---|
| Chain of Responsibility, Mediator, Observer | 발신자와 수신자를 연결하는 다양한 방법을 제공 |
| Memento | Command와 함께 “실행 취소”를 구현할 때 사용. 명령이 작업을 수행하고, Memento가 이전 상태를 저장 |
| Strategy | 둘 다 객체를 매개변수화하지만 의도가 다름. Command는 작업을 객체로 변환, Strategy는 알고리즘 교체 |
| Prototype | 명령을 히스토리에 저장하기 전에 복사할 때 Prototype 사용 |
| Visitor | Command의 강력한 버전으로 볼 수 있음. 다양한 클래스의 객체에 대해 작업 실행 가능 |