Chat Conversation
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를 사용하되, 복잡한 애플리케이션에서는 두 패턴을 조합해서 사용할 수도 있습니다.