Menu
Coddy logo textTech

커맨드 패턴

Coddy Dart 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 110개 중 94번째.

커맨드 패턴(Command pattern)은 요청을 객체로 캡슐화하여, 서로 다른 요청을 가진 클라이언트를 매개변수화하거나, 작업을 큐에 저장하거나, 실행 취소 기능을 지원할 수 있게 합니다. 메서드를 직접 호출하는 대신, 작업을 커맨드 객체로 감싸서 저장하거나 전달하거나 나중에 실행할 수 있도록 합니다.

이 패턴은 세 가지 핵심 구성 요소를 포함합니다: execute 메서드가 있는 Command 인터페이스, 특정 작업을 구현하는 Concrete Commands, 그리고 실제 작업을 수행하는 Receiver입니다. 선택 사항인 Invoker는 명령이 무엇을 하는지 알지 못한 채 명령을 트리거합니다:

abstract class Command {
  void execute();
}

class Light {
  void turnOn() => print('Light is ON');
  void turnOff() => print('Light is OFF');
}

class TurnOnCommand implements Command {
  final Light light;
  TurnOnCommand(this.light);

  @override
  void execute() => light.turnOn();
}

class TurnOffCommand implements Command {
  final Light light;
  TurnOffCommand(this.light);

  @override
  void execute() => light.turnOff();
}

class RemoteControl {
  Command? _command;

  void setCommand(Command command) {
    _command = command;
  }

  void pressButton() {
    _command?.execute();
  }
}

void main() {
  var light = Light();
  var remote = RemoteControl();

  remote.setCommand(TurnOnCommand(light));
  remote.pressButton();  // Light is ON

  remote.setCommand(TurnOffCommand(light));
  remote.pressButton();  // Light is OFF
}

RemoteControl은 조명에 대해 아무것도 모릅니다. 단지 주어진 명령이 무엇이든 실행할 뿐입니다. 이러한 디커플링(decoupling) 덕분에 인보커(invoker)를 수정하지 않고도 새로운 명령을 쉽게 추가할 수 있습니다. 커맨드 패턴은 작업을 일급 객체(first-class objects)로 취급해야 하는 실행 취소/다시 실행 시스템, 작업 큐 또는 매크로 기록 등을 구현하는 데 이상적입니다.

challenge icon

챌린지

쉬움

커맨드 패턴을 사용하여 텍스트 에디터 명령 시스템을 구축해 봅시다! 텍스트 작업을 커맨드 객체로 캡슐화하는 시스템을 만들게 됩니다. 이를 통해 에디터는 각 작업의 세부 사항을 알지 못해도 다양한 동작을 실행할 수 있습니다. 이것은 커맨드 패턴의 전형적인 사용 사례입니다. 각 동작이 객체로 저장되는 실행 취소/다시 실행(undo/redo) 시스템을 생각하면 이해하기 쉽습니다.

코드는 다음 두 개의 파일로 구성합니다:

  • commands.dart: 여기에 커맨드 시스템을 만듭니다. 먼저 execute() 메서드를 선언하는 추상 클래스 Command를 시작으로 작성하세요. 그런 다음 String content 필드(초기값은 빈 문자열)를 가지고 다음 세 가지 메서드를 포함하는 TextDocument 클래스(수신자, receiver)를 만듭니다:
    • append(String text) - content의 끝에 텍스트를 추가합니다.
    • clear() - content를 빈 문자열로 설정합니다.
    • show() - Document: [content]를 출력합니다.
    이제 TextDocument와 함께 작동하는 세 가지 구체적인 커맨드(concrete commands)를 구현하세요:
    • AppendCommand - 생성자에서 문서와 텍스트를 전달받고, 실행 시 해당 텍스트를 추가합니다.
    • ClearCommand - 생성자에서 문서를 전달받고, 실행 시 문서를 비웁니다.
    • ShowCommand - 생성자에서 문서를 전달받고, 실행 시 문서의 내용을 보여줍니다.
    마지막으로 커맨드 목록을 저장하는 TextEditor 클래스(호출자, invoker)를 만듭니다. 커맨드를 큐에 추가하는 addCommand(Command command) 메서드와 큐에 있는 모든 커맨드를 순서대로 실행하는 runAll() 메서드가 있어야 합니다.
  • main.dart: 커맨드 파일을 임포트하고 커맨드 패턴을 시연합니다. TextDocumentTextEditor를 생성하세요. 다음 커맨드들을 순서대로 큐에 추가합니다: Hello 추가, World 추가, 문서 표시, ! 추가, 다시 표시, 문서 비우기, 그리고 마지막으로 한 번 더 표시합니다. 그런 다음 대기 중인 모든 커맨드를 실행합니다.

TextEditor가 추가, 비우기, 표시하기에 대해 전혀 알지 못하고 단지 주어진 커맨드를 실행할 뿐이라는 점에 주목하세요. 이러한 디커플링(결합도 낮추기)이 커맨드 패턴의 핵심입니다!

예상 출력:

Document: Hello World
Document: Hello World!
Document: 

치트 시트

커맨드 패턴(Command pattern)은 요청을 객체로 캡슐화하여, 서로 다른 요청으로 클라이언트를 매개변수화하거나, 작업을 큐에 넣거나, 실행 취소 기능을 지원할 수 있게 합니다.

이 패턴은 네 가지 핵심 구성 요소를 포함합니다:

  • execute 메서드를 가진 Command 인터페이스
  • 특정 작업을 구현하는 Concrete Commands
  • 실제 작업을 수행하는 Receiver
  • 명령이 무엇을 하는지 알지 못한 채 명령을 트리거하는 Invoker (선택 사항)

기본 구조:

abstract class Command {
  void execute();
}

class Receiver {
  void action() => print('Action performed');
}

class ConcreteCommand implements Command {
  final Receiver receiver;
  ConcreteCommand(this.receiver);

  @override
  void execute() => receiver.action();
}

class Invoker {
  Command? _command;

  void setCommand(Command command) {
    _command = command;
  }

  void trigger() {
    _command?.execute();
  }
}

조명 제어 시스템을 사용한 예시:

class Light {
  void turnOn() => print('Light is ON');
  void turnOff() => print('Light is OFF');
}

class TurnOnCommand implements Command {
  final Light light;
  TurnOnCommand(this.light);

  @override
  void execute() => light.turnOn();
}

class RemoteControl {
  Command? _command;

  void setCommand(Command command) {
    _command = command;
  }

  void pressButton() {
    _command?.execute();
  }
}

void main() {
  var light = Light();
  var remote = RemoteControl();

  remote.setCommand(TurnOnCommand(light));
  remote.pressButton();  // Light is ON
}

인보커(invoker)는 리시버(receiver)에 대해 아무것도 알지 못하며, 단지 주어진 명령을 실행할 뿐입니다. 이러한 디커플링(decoupling) 덕분에 인보커를 수정하지 않고도 새로운 명령을 쉽게 추가할 수 있습니다.

직접 해보기

import 'commands.dart';

void main() {
  // TODO: TextDocument 인스턴스 생성

  // TODO: TextEditor 인스턴스 생성

  // TODO: 다음 명령들을 순서대로 큐에 추가하세요:
  // 1. "Hello" 추가
  // 2. " World" 추가
  // 3. 문서 표시
  // 4. "!" 추가
  // 5. 문서를 다시 표시
  // 6. 문서 내용 지우기
  // 7. 마지막으로 문서 표시

  // TODO: 대기열의 모든 명령 실행
}
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

객체 지향 프로그래밍의 모든 레슨