커맨드 command 패턴 - 타입스크립트 예시

SW설계
읽는데 5분 소요
처음 쓰여진 날: 2025-09-01
마지막 수정일: 2025-09-01

요약

커맨드 command 패턴을 타입스크립트 코드와 함께 알아봅니다.

커맨드 (Command) 패턴 요약

패턴 종류핵심 키워드
커맨드 (Command)요청(request) 그 자체를 객체로 캡슐화
커맨드 패턴 감자
커맨드 패턴 감자

커맨드 (Command) 패턴

커맨드 패턴은 '요청(request)' 그 자체를 객체로 캡슐화하여, 요청을 보내는 객체(Invoker)와 요청을 실제로 처리하는 객체(Receiver)를 분리하는 패턴입니다.

가장 흔한 비유는 'TV 리모컨' 입니다. 리모컨(Invoker)의 버튼들은 각각 '전원 켜기', '볼륨 높이기' 같은 기능(Command)을 가지고 있습니다. 우리가 버튼을 누르면, 리모컨은 해당 기능이 담긴 신호를 TV(Receiver)에 보냅니다. 리모컨은 TV가 내부적으로 어떻게 동작하는지 전혀 모르며, 그저 정해진 신호를 보낼 뿐입니다. TV 또한 리모컨이 어떻게 생겼는지, 버튼이 몇 개인지 신경 쓰지 않고 들어온 신호를 처리하기만 하면 됩니다.

이처럼 커맨드 패턴은 '무엇을 할 것인가''누가, 어떻게 할 것인가' 로부터 분리하여 시스템의 유연성을 크게 높입니다.

기본 구조

  • Command: 모든 구체적인 커맨드 클래스들이 구현해야 하는 공통 인터페이스입니다. 보통 execute()라는 단일 메서드를 가집니다.
  • ConcreteCommand: Command 인터페이스를 구현하며, Receiver 객체에 대한 참조를 가집니다. execute()가 호출되면, Receiver의 특정 메서드를 호출하여 요청을 실행합니다.
  • Invoker: 사용자(클라이언트)의 요청을 받아 Command 객체를 실행하는 역할을 합니다. (예: 리모컨, 버튼) InvokerConcreteCommand가 아닌 Command 인터페이스에만 의존하므로, 어떤 커맨드든 실행할 수 있습니다.
  • Receiver: 요청을 실제로 수행하는 객체입니다. 비즈니스 로직을 포함하고 있습니다. (예: TV, 전등)

예시: 스마트홈 리모컨 만들기

전등을 켜고 끄는 간단한 스마트홈 리모컨을 만든다고 가정해봅시다. 여기에 커맨드 패턴의 꽃이라 불리는 '실행 취소(Undo)' 기능까지 추가해 보겠습니다.

먼저, 요청을 실제로 처리할 ReceiverLight 클래스를 정의합니다.

typescript
// Receiver: 요청을 실제로 처리하는 객체
class Light {
  turnOn() {
    console.log("💡 전등이 켜졌습니다.");
  }

  turnOff() {
    console.log("⬛ 전등이 꺼졌습니다.");
  }
}

다음으로 모든 커맨드가 따를 Command 인터페이스를 정의합니다. undo() 메서드를 추가하여 실행 취소 기능을 지원하도록 합니다.

typescript
// Command: 모든 커맨드 객체가 구현할 인터페이스
interface Command {
  execute(): void;
  undo(): void;
}

이제 구체적인 커맨드, LightOnCommandLightOffCommand를 만듭니다.

typescript
// ConcreteCommand: 전등 켜기
class LightOnCommand implements Command {
  private light: Light;

  constructor(light: Light) {
    this.light = light;
  }

  execute(): void {
    this.light.turnOn();
  }

  undo(): void {
    this.light.turnOff();
  }
}

// ConcreteCommand: 전등 끄기
class LightOffCommand implements Command {
  private light: Light;

  constructor(light: Light) {
    this.light = light;
  }

  execute(): void {
    this.light.turnOff();
  }

  undo(): void {
    this.light.turnOn();
  }
}

마지막으로, 커맨드를 받아 실행하고, 실행 내역을 기록하여 Undo 기능을 처리할 InvokerRemoteControl 클래스를 만듭니다.

typescript
// Invoker: 커맨드를 실행하고, 실행 취소를 지원하는 리모컨
class RemoteControl {
  private command: Command | null = null;
  private commandHistory: Command[] = [];

  setCommand(command: Command): void {
    this.command = command;
  }

  pressButton(): void {
    if (this.command) {
      this.command.execute();
      this.commandHistory.push(this.command); // 실행 기록 저장
    } else {
      console.log("설정된 커맨드가 없습니다.");
    }
  }

  pressUndoButton(): void {
    const lastCommand = this.commandHistory.pop();
    if (lastCommand) {
      console.log("--- 실행 취소 ---");
      lastCommand.undo();
    } else {
      console.log("취소할 작업이 없습니다.");
    }
  }
}

// 클라이언트 코드
const remote = new RemoteControl();
const livingRoomLight = new Light();

const lightOn = new LightOnCommand(livingRoomLight);
const lightOff = new LightOffCommand(livingRoomLight);

// 전등 켜기
remote.setCommand(lightOn);
remote.pressButton(); // 출력: 💡 전등이 켜졌습니다.

// 전등 끄기
remote.setCommand(lightOff);
remote.pressButton(); // 출력: ⬛ 전등이 꺼졌습니다.

// 실행 취소 (가장 마지막 작업인 '전등 끄기'를 취소)
remote.pressUndoButton();
// 출력:
// --- 실행 취소 ---
// 💡 전등이 켜졌습니다.

// 다시 실행 취소 (그 이전 작업인 '전등 켜기'를 취소)
remote.pressUndoButton();
// 출력:
// --- 실행 취소 ---
// ⬛ 전등이 꺼졌습니다.

RemoteControlLight 객체의 존재를 전혀 모릅니다. 그저 Command 인터페이스의 execute()undo() 메서드를 호출할 뿐입니다. 만약 오디오를 제어하는 기능을 추가하고 싶다면, Audio 클래스와 AudioOnCommand 등을 새로 만들어 리모컨에 설정하기만 하면 됩니다. 리모컨 코드는 변경할 필요가 없습니다.

커맨드 패턴 중요 키워드

  • 요청을 객체로 캡슐화합니다.
  • 요청자(Invoker)와 수신자(Receiver)를 분리하여 결합도를 낮춥니다.
  • 높은 재사용성: InvokerCommand 인터페이스에만 의존하므로, 어떤 기능(Command)이든 실행할 수 있는 재사용 가능한 클래스가 됩니다. 예시의 RemoteControlLight뿐만 아니라 Audio, Heater 등 어떤 Receiver의 커맨드든 받아 실행할 수 있습니다.
  • 실행 취소(Undo/Redo) 기능을 구현하는 데 매우 유용합니다.
  • 작업 큐, 트랜잭션, 로깅 등 다양한 곳에 활용될 수 있습니다.
문제
요청(request) 그 자체를 객체로 캡슐화하여 요청을 보내는 객체와 요청을 실제로 처리하는 객체를 분리하는 패턴으로, 실행 취소(Undo) 기능이나 작업 큐를 구현하는 데 매우 유용한 디자인 패턴은?
보기
답변
정답정답 보기