Skip to content
2글자 이상 입력하세요
behavioral으로

Command

13 min 읽기
behavioral-patternscommandactiontransactiondesign-patterns

Command

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

Command pattern

Intent

Command 패턴은 특정 메서드 호출을 독립 객체로 변환하여 다양한 이점을 제공합니다:

Problem

텍스트 에디터 앱을 개발하고 있다고 가정합시다. 다양한 작업을 위한 버튼이 있는 도구 모음을 만들어야 합니다.

버튼 서브클래스 문제

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

많은 서브클래스

가장 간단한 해결책은 각 버튼 유형에 대해 수많은 서브클래스를 만드는 것입니다. 하지만 이 접근 방식에는 문제가 있습니다:

GUI와 비즈니스 로직 결합

더 나쁜 문제가 있습니다. 복사/붙여넣기 같은 작업은 여러 곳에서 호출됩니다:

이로 인해 코드가 중복되거나 GUI가 비즈니스 로직에 의존하게 됩니다.

Solution

좋은 소프트웨어 설계는 종종 관심사의 분리 원칙을 기반으로 합니다. GUI 레이어와 비즈니스 로직 레이어를 분리해야 합니다.

GUI 레이어

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

요청을 명령으로 추출

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

공통 명령 인터페이스

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

Real-World Analogy

레스토랑 주문

도시를 오래 걸은 후 좋은 레스토랑에 들어가 창가 테이블에 앉습니다. 친절한 웨이터가 다가와 주문을 받아 종이에 적습니다.

웨이터는 주방으로 가서 주문을 벽에 붙입니다. 잠시 후 주문이 요리사에게 전달되고, 요리사가 읽고 요리를 준비합니다.

종이 주문은 명령 역할을 합니다:

Structure

Command 구조

Command 구조 상세

구성 요소역할
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

  1. Command 인터페이스 선언: 단일 실행 메서드로 명령 인터페이스를 선언합니다.

  2. 구체적인 명령 추출: 요청을 명령 인터페이스를 구현하는 구체적인 명령 클래스로 추출합니다. 각 클래스에는 요청 인수와 실제 수신자 참조를 저장하는 필드가 있어야 합니다.

  3. Sender 식별: 발신자 역할을 할 클래스를 식별합니다. 이 클래스에 명령을 저장할 필드를 추가합니다. 발신자는 명령 인터페이스를 통해서만 명령과 통신해야 합니다.

  4. Sender 수정: 발신자가 수신자에게 직접 요청을 보내는 대신 명령을 실행하도록 수정합니다.

  5. 클라이언트 초기화: 클라이언트는 다음 순서로 객체를 초기화해야 합니다:

    • 수신자 생성
    • 명령 생성 및 수신자 연결
    • 발신자 생성 및 특정 명령 연결

Pros and Cons

장점

장점설명
Single Responsibility Principle작업을 호출하는 클래스와 수행하는 클래스를 분리할 수 있습니다
Open/Closed Principle기존 클라이언트 코드를 수정하지 않고 새 명령을 도입할 수 있습니다
Undo/Redo 구현실행 취소/다시 실행을 구현할 수 있습니다
지연 실행작업의 지연 실행을 구현할 수 있습니다
명령 조합간단한 명령들을 조합하여 복잡한 명령을 만들 수 있습니다

단점

단점설명
코드 복잡성발신자와 수신자 사이에 완전히 새로운 레이어를 도입하므로 코드가 복잡해질 수 있습니다

Relations with Other Patterns

패턴관계
Chain of Responsibility, Mediator, Observer발신자와 수신자를 연결하는 다양한 방법을 제공
MementoCommand와 함께 “실행 취소”를 구현할 때 사용. 명령이 작업을 수행하고, Memento가 이전 상태를 저장
Strategy둘 다 객체를 매개변수화하지만 의도가 다름. Command는 작업을 객체로 변환, Strategy는 알고리즘 교체
Prototype명령을 히스토리에 저장하기 전에 복사할 때 Prototype 사용
VisitorCommand의 강력한 버전으로 볼 수 있음. 다양한 클래스의 객체에 대해 작업 실행 가능

출처: refactoring.guru - Command


이전 글

Chain of Responsibility

다음 글

Composite