Chat Conversation

참여자: User, QueryPie

Message 1 from User

command pattern에 대해서 typescript 예제로 자세히 설명해줘

Message 2 from QueryPie

Command 패턴은 요청을 객체로 캡슐화하여 요청을 매개변수화하고, 큐에 저장하거나 로깅하고, 실행 취소 기능을 지원할 수 있게 하는 행동 디자인 패턴입니다. 이 패턴은 요청자(invoker)와 수신자(receiver)를 분리하여 시스템의 유연성을 높입니다.

Command 패턴의 핵심 구성 요소

Command Interface: 모든 명령이 구현해야 하는 공통 인터페이스 Concrete Command: 실제 명령을 구현하는 클래스 Receiver: 실제 작업을 수행하는 객체 Invoker: 명령을 호출하는 객체 Client: 명령 객체를 생성하고 설정하는 역할

TypeScript 구현 예제

1. 기본 Command 패턴 구조

// Command 인터페이스
interface Command {
  execute(): void;
  undo(): void;
}

// Receiver - 실제 작업을 수행하는 클래스
class TextEditor {
  private content: string = "";
  
  write(text: string): void {
    this.content += text;
  }
  
  delete(length: number): void {
    this.content = this.content.slice(0, -length);
  }
  
  getContent(): string {
    return this.content;
  }
  
  setContent(content: string): void {
    this.content = content;
  }
}

// Concrete Command - 텍스트 작성 명령
class WriteCommand implements Command {
  private textEditor: TextEditor;
  private text: string;
  
  constructor(textEditor: TextEditor, text: string) {
    this.textEditor = textEditor;
    this.text = text;
  }
  
  execute(): void {
    this.textEditor.write(this.text);
  }
  
  undo(): void {
    this.textEditor.delete(this.text.length);
  }
}

// Concrete Command - 텍스트 삭제 명령
class DeleteCommand implements Command {
  private textEditor: TextEditor;
  private deletedText: string = "";
  private length: number;
  
  constructor(textEditor: TextEditor, length: number) {
    this.textEditor = textEditor;
    this.length = length;
  }
  
  execute(): void {
    const content = this.textEditor.getContent();
    this.deletedText = content.slice(-this.length);
    this.textEditor.delete(this.length);
  }
  
  undo(): void {
    this.textEditor.write(this.deletedText);
  }
}

// Invoker - 명령을 관리하고 실행하는 클래스
class EditorInvoker {
  private history: Command[] = [];
  private currentPosition: number = -1;
  
  execute(command: Command): void {
    // 현재 위치 이후의 명령들 제거 (새로운 명령 실행 시)
    this.history = this.history.slice(0, this.currentPosition + 1);
    
    command.execute();
    this.history.push(command);
    this.currentPosition++;
  }
  
  undo(): void {
    if (this.currentPosition >= 0) {
      const command = this.history[this.currentPosition];
      command.undo();
      this.currentPosition--;
    }
  }
  
  redo(): void {
    if (this.currentPosition < this.history.length - 1) {
      this.currentPosition++;
      const command = this.history[this.currentPosition];
      command.execute();
    }
  }
}

2. 사용 예제

// 사용법
const editor = new TextEditor();
const invoker = new EditorInvoker();

// 명령 실행
const writeHello = new WriteCommand(editor, "Hello ");
const writeWorld = new WriteCommand(editor, "World!");
const deleteChars = new DeleteCommand(editor, 6);

invoker.execute(writeHello);
console.log(editor.getContent()); // "Hello "

invoker.execute(writeWorld);
console.log(editor.getContent()); // "Hello World!"

invoker.execute(deleteChars);
console.log(editor.getContent()); // "Hello "

// Undo/Redo 기능
invoker.undo();
console.log(editor.getContent()); // "Hello World!"

invoker.undo();
console.log(editor.getContent()); // "Hello "

invoker.redo();
console.log(editor.getContent()); // "Hello World!"

3. 매크로 명령 (복합 명령) 구현

class MacroCommand implements Command {
  private commands: Command[] = [];
  
  add(command: Command): void {
    this.commands.push(command);
  }
  
  execute(): void {
    this.commands.forEach(command => command.execute());
  }
  
  undo(): void {
    // 역순으로 undo 실행
    for (let i = this.commands.length - 1; i >= 0; i--) {
      this.commands[i].undo();
    }
  }
}

// 매크로 사용 예제
const macro = new MacroCommand();
macro.add(new WriteCommand(editor, "TypeScript "));
macro.add(new WriteCommand(editor, "is "));
macro.add(new WriteCommand(editor, "awesome!"));

invoker.execute(macro);
console.log(editor.getContent()); // 이전 내용 + "TypeScript is awesome!"

4. 실제 애플리케이션 예제 - 스마트 홈 제어

// Receiver 클래스들
class Light {
  private isOn: boolean = false;
  
  turnOn(): void {
    this.isOn = true;
    console.log("Light is ON");
  }
  
  turnOff(): void {
    this.isOn = false;
    console.log("Light is OFF");
  }
  
  getStatus(): boolean {
    return this.isOn;
  }
}

class AirConditioner {
  private temperature: number = 25;
  private isOn: boolean = false;
  
  turnOn(): void {
    this.isOn = true;
    console.log("AC is ON");
  }
  
  turnOff(): void {
    this.isOn = false;
    console.log("AC is OFF");
  }
  
  setTemperature(temp: number): void {
    this.temperature = temp;
    console.log(`Temperature set to ${temp}°C`);
  }
  
  getTemperature(): number {
    return this.temperature;
  }
}

// Command 구현
class LightOnCommand implements Command {
  constructor(private light: Light) {}
  
  execute(): void {
    this.light.turnOn();
  }
  
  undo(): void {
    this.light.turnOff();
  }
}

class LightOffCommand implements Command {
  constructor(private light: Light) {}
  
  execute(): void {
    this.light.turnOff();
  }
  
  undo(): void {
    this.light.turnOn();
  }
}

class SetTemperatureCommand implements Command {
  private previousTemp: number;
  
  constructor(
    private ac: AirConditioner, 
    private temperature: number
  ) {
    this.previousTemp = ac.getTemperature();
  }
  
  execute(): void {
    this.previousTemp = this.ac.getTemperature();
    this.ac.setTemperature(this.temperature);
  }
  
  undo(): void {
    this.ac.setTemperature(this.previousTemp);
  }
}

// 스마트 홈 컨트롤러
class SmartHomeController {
  private commands: { [key: string]: Command } = {};
  private lastCommand: Command | null = null;
  
  setCommand(slot: string, command: Command): void {
    this.commands[slot] = command;
  }
  
  pressButton(slot: string): void {
    if (this.commands[slot]) {
      this.commands[slot].execute();
      this.lastCommand = this.commands[slot];
    }
  }
  
  pressUndo(): void {
    if (this.lastCommand) {
      this.lastCommand.undo();
    }
  }
}

// 사용 예제
const livingRoomLight = new Light();
const aircon = new AirConditioner();
const controller = new SmartHomeController();

// 명령 설정
controller.setCommand("light_on", new LightOnCommand(livingRoomLight));
controller.setCommand("light_off", new LightOffCommand(livingRoomLight));
controller.setCommand("cool", new SetTemperatureCommand(aircon, 20));

// 명령 실행
controller.pressButton("light_on");  // Light is ON
controller.pressButton("cool");      // Temperature set to 20°C
controller.pressUndo();              // Temperature set to 25°C (이전 온도)

Command 패턴의 장점과 사용 시기

장점:

  • 분리: 요청자와 수신자의 결합도 감소
  • 확장성: 새로운 명령 추가가 쉬움
  • 실행 취소: Undo/Redo 기능 구현 용이
  • 로깅: 명령 실행 기록 관리 가능
  • 큐잉: 명령을 큐에 저장하여 나중에 실행 가능

사용하면 좋은 경우:

  • GUI 버튼과 메뉴 항목
  • 실행 취소/다시 실행 기능이 필요한 경우
  • 요청을 큐에 저장해야 하는 경우
  • 매크로 명령 구현
  • 로깅 및 트랜잭션 처리

Command 패턴은 특히 사용자 인터페이스나 에디터 애플리케이션에서 매우 유용하며, 시스템의 유연성과 확장성을 크게 향상시킵니다.

Message 3 from User

task 패턴과의 차이는 뭐야?

Message 4 from QueryPie

Task 패턴과 Command 패턴은 비슷해 보이지만 목적과 사용 방식에서 중요한 차이가 있습니다.

주요 차이점 비교

구분 Command 패턴 Task 패턴
목적 요청을 객체로 캡슐화하여 분리 비동기 작업의 표현과 관리
실행 시점 즉시 또는 나중에 실행 주로 비동기 실행
상태 관리 Undo/Redo 지원 완료/실패/진행 상태
반환값 보통 void Promise 또는 결과값 반환
주 사용처 GUI, 매크로, 실행취소 비동기 처리, 병렬 작업

TypeScript로 비교 예제

Command 패턴 (앞서 설명한 것)

interface Command {
  execute(): void;
  undo(): void;
}

class SaveFileCommand implements Command {
  constructor(private filename: string, private content: string) {}
  
  execute(): void {
    console.log(`Saving ${this.filename}`);
    // 파일 저장 로직
  }
  
  undo(): void {
    console.log(`Reverting save of ${this.filename}`);
    // 저장 취소 로직
  }
}

// 사용법
const saveCommand = new SaveFileCommand("document.txt", "Hello World");
saveCommand.execute(); // 즉시 실행
saveCommand.undo();    // 실행 취소

Task 패턴

// Task 인터페이스
interface Task<T> {
  execute(): Promise<T>;
  cancel?(): void;
  onProgress?(callback: (progress: number) => void): void;
}

// Concrete Task
class FileDownloadTask implements Task<string> {
  private cancelled: boolean = false;
  private progressCallback?: (progress: number) => void;
  
  constructor(private url: string) {}
  
  async execute(): Promise<string> {
    return new Promise((resolve, reject) => {
      console.log(`Starting download from ${this.url}`);
      
      // 비동기 다운로드 시뮬레이션
      let progress = 0;
      const interval = setInterval(() => {
        if (this.cancelled) {
          clearInterval(interval);
          reject(new Error('Download cancelled'));
          return;
        }
        
        progress += 10;
        this.progressCallback?.(progress);
        
        if (progress >= 100) {
          clearInterval(interval);
          resolve('Downloaded content');
        }
      }, 100);
    });
  }
  
  cancel(): void {
    this.cancelled = true;
    console.log('Download cancelled');
  }
  
  onProgress(callback: (progress: number) => void): void {
    this.progressCallback = callback;
  }
}

// Task Manager
class TaskManager {
  private tasks: Map<string, Task<any>> = new Map();
  
  async runTask<T>(id: string, task: Task<T>): Promise<T> {
    this.tasks.set(id, task);
    
    try {
      const result = await task.execute();
      this.tasks.delete(id);
      return result;
    } catch (error) {
      this.tasks.delete(id);
      throw error;
    }
  }
  
  cancelTask(id: string): void {
    const task = this.tasks.get(id);
    if (task && task.cancel) {
      task.cancel();
    }
  }
  
  getAllTasks(): string[] {
    return Array.from(this.tasks.keys());
  }
}

// 사용 예제
async function demonstrateTaskPattern() {
  const taskManager = new TaskManager();
  const downloadTask = new FileDownloadTask('https://example.com/file.zip');
  
  // 진행상황 모니터링
  downloadTask.onProgress((progress) => {
    console.log(`Progress: ${progress}%`);
  });
  
  try {
    // 비동기 실행
    const result = await taskManager.runTask('download1', downloadTask);
    console.log('Download completed:', result);
  } catch (error) {
    console.error('Download failed:', error.message);
  }
}

실제 사용 시나리오 비교

Command 패턴이 적합한 경우

// 텍스트 에디터의 편집 명령들
class TextEditorApp {
  private invoker = new EditorInvoker();
  private editor = new TextEditor();
  
  // 메뉴 클릭 핸들러
  onBoldClick() {
    const command = new FormatCommand(this.editor, 'bold');
    this.invoker.execute(command);
  }
  
  onUndoClick() {
    this.invoker.undo(); // 이전 명령 취소
  }
  
  onRedoClick() {
    this.invoker.redo(); // 명령 다시 실행
  }
}

Task 패턴이 적합한 경우

// 파일 업로드 애플리케이션
class FileUploadApp {
  private taskManager = new TaskManager();
  
  async uploadFile(file: File) {
    const uploadTask = new FileUploadTask(file);
    
    // 진행률 표시
    uploadTask.onProgress((progress) => {
      this.updateProgressBar(progress);
    });
    
    try {
      const result = await this.taskManager.runTask(
        `upload-${file.name}`, 
        uploadTask
      );
      this.showSuccess(result);
    } catch (error) {
      this.showError(error.message);
    }
  }
  
  cancelUpload(filename: string) {
    this.taskManager.cancelTask(`upload-${filename}`);
  }
}

복합적 사용 예제

두 패턴을 함께 사용하는 경우도 있습니다:

// Task를 실행하는 Command
class AsyncTaskCommand implements Command {
  private task: Task<any>;
  private result: any;
  private completed: boolean = false;
  
  constructor(task: Task<any>) {
    this.task = task;
  }
  
  async execute(): Promise<void> {
    try {
      this.result = await this.task.execute();
      this.completed = true;
    } catch (error) {
      console.error('Task execution failed:', error);
    }
  }
  
  undo(): void {
    if (this.completed && this.task.cancel) {
      // 완료된 작업의 결과를 되돌리는 로직
      console.log('Undoing completed task');
    }
  }
}

// 사용 예제
const downloadTask = new FileDownloadTask('https://example.com/data.json');
const asyncCommand = new AsyncTaskCommand(downloadTask);

// Command 패턴의 이점 (undo/redo)과 Task 패턴의 이점 (비동기 처리) 결합
await asyncCommand.execute();

요약

Command 패턴은 사용자 행동을 객체로 만들어 실행 취소, 매크로, 로깅 등을 지원하는 데 중점을 둡니다. 주로 동기적이고 즉시 실행됩니다.

Task 패턴은 비동기 작업을 표현하고 관리하는 데 중점을 둡니다. 진행 상황 추적, 취소, 병렬 실행 등을 지원합니다.

선택 기준은 실행 취소가 필요하면 Command, 비동기 처리가 필요하면 Task를 사용하되, 복잡한 애플리케이션에서는 두 패턴을 조합해서 사용할 수도 있습니다.