커맨드 패턴
Coddy C++ 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 104개 중 96번째.
커맨드 패턴(Command pattern)은 요청을 객체로 캡슐화하여, 서로 다른 요청을 가진 클라이언트를 매개변수화하거나, 작업을 큐에 저장하거나, 실행 취소 기능을 지원할 수 있게 해줍니다. 이는 작업을 호출하는 객체와 작업을 수행하는 방법을 아는 객체를 분리합니다.
이 패턴은 네 가지 핵심 구성 요소로 이루어져 있습니다: execute() 메서드를 가진 Command 인터페이스, 구체적인 동작을 구현하는 Concrete Commands, 실제 작업을 수행하는 Receiver, 그리고 명령을 트리거하는 Invoker입니다:
#include <iostream>
#include <memory>
#include <vector>
// 수신자(Receiver) - 연산을 수행하는 방법을 알고 있습니다.
class Light {
public:
void turnOn() { std::cout << "Light is ON\n"; }
void turnOff() { std::cout << "Light is OFF\n"; }
};
// 커맨드(Command) 인터페이스
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
// 구체적인 커맨드(Concrete Commands)
class LightOnCommand : public Command {
Light& light;
public:
LightOnCommand(Light& l) : light(l) {}
void execute() override { light.turnOn(); }
void undo() override { light.turnOff(); }
};
class LightOffCommand : public Command {
Light& light;
public:
LightOffCommand(Light& l) : light(l) {}
void execute() override { light.turnOff(); }
void undo() override { light.turnOn(); }
};
// 호출자(Invoker)
class RemoteControl {
std::vector<std::unique_ptr<Command>> history;
public:
void pressButton(std::unique_ptr<Command> cmd) {
cmd->execute();
history.push_back(std::move(cmd));
}
void pressUndo() {
if (!history.empty()) {
history.back()->undo();
history.pop_back();
}
}
};각 커맨드는 수신자(receiver)에 대한 참조를 저장하고 실행될 때 적절한 메서드를 호출합니다. 인보커(invoker)는 어떤 동작이 일어날지 알지 못하며, 단지 execute()를 호출할 뿐입니다. 커맨드들을 히스토리에 저장함으로써, 각 커맨드의 undo() 메서드를 호출하여 실행 취소(undo)를 쉽게 구현할 수 있습니다.
작업을 큐에 넣거나, 실행 취소/다시 실행을 구현하거나, 요청의 발신자와 처리자를 분리해야 할 때 Command 패턴을 사용하세요.
챌린지
쉬움커맨드 패턴을 사용하여 실행 취소(undo) 기능이 있는 텍스트 에디터를 만들어 보겠습니다. 텍스트 삽입 및 삭제와 같은 편집 작업을 커맨드 객체로 캡슐화하여, 사용자가 작업을 실행하고 실제 텍스트 에디터처럼 역순으로 실행 취소할 수 있는 시스템을 구축하게 됩니다.
코드는 다음 네 개의 파일로 구성됩니다:
TextDocument.h: 실제 텍스트 내용을 보유하고 조작하는 수신자(receiver) 클래스를 생성합니다.TextDocument클래스는 문서의 내용을 문자열로 저장해야 합니다. 다음 메서드들을 구현하세요:insertText(const std::string& text)— 내용의 끝에 텍스트를 추가합니다.deleteText(int count)— 내용의 끝에서count만큼의 문자를 제거합니다 (count가 내용의 길이보다 크면 아무 작업도 하지 않습니다).getContent()— 현재 내용을 const 참조로 반환합니다.
Command.h: 커맨드 인터페이스와 구체적인 커맨드 클래스들을 정의합니다.순수 가상 메서드인
execute()와undo(), 그리고 가상 소멸자를 가진 추상Command클래스를 생성하세요.그런 다음 두 개의 구체적인 커맨드를 구현하세요:
InsertCommand—TextDocument에 대한 참조와 삽입할 문자열을 인자로 받습니다. 실행될 때 텍스트를 삽입합니다. 실행 취소될 때는 삽입되었던 것과 동일한 수의 문자를 삭제합니다.DeleteCommand—TextDocument에 대한 참조와 삭제할 문자 수(count)를 인자로 받습니다. 실행 취소 시 복구할 수 있도록 삭제된 텍스트를 저장해야 합니다. 실행될 때 끝에서부터 문자를 제거합니다. 실행 취소될 때 저장된 텍스트를 다시 삽입합니다.
TextEditor.h: 커맨드 실행 및 히스토리를 관리하는 호출자(invoker)를 생성합니다.TextEditor클래스는TextDocument에 대한 참조를 보유하고,unique_ptr<Command>의 벡터를 사용하여 실행된 커맨드의 히스토리를 유지해야 합니다. 다음을 구현하세요:executeCommand(std::unique_ptr<Command> cmd)— 커맨드를 실행하고 히스토리에 추가합니다.undo()— 가장 최근의 커맨드를 실행 취소하고 히스토리에서 제거합니다 (히스토리가 비어 있으면 아무 작업도 하지 않습니다).showContent()— 현재 문서 내용을 출력하거나, 내용이 비어 있으면[empty]를 출력합니다.
main.cpp: 실행 취소 기능이 포함된 커맨드 패턴을 시연합니다.세 개의 입력을 읽습니다:
- 첫 번째로 삽입할 텍스트 (문자열)
- 두 번째로 삽입할 텍스트 (문자열)
- 삭제할 문자 수 (정수)
TextDocument와TextEditor를 생성합니다. 그런 다음 아래 작업을 순서대로 수행하고, 각 단계 후에 내용을 출력하세요:- 첫 번째 텍스트를 삽입한 후 내용을 출력합니다.
- 두 번째 텍스트를 삽입한 후 내용을 출력합니다.
- 지정된 수만큼 문자를 삭제한 후 내용을 출력합니다.
- 한 번 실행 취소한 후 내용을 출력합니다.
- 한 번 더 실행 취소한 후 내용을 출력합니다.
예를 들어, 입력값이 Hello, World, 3인 경우:
Hello
Hello World
Hello Wo
Hello World
Hello입력값이 Code, Editor, 6인 경우:
Code
CodeEditor
Code
CodeEditor
Code각 커맨드가 자신을 어떻게 되돌리는지 알고 있다는 점에 주목하세요. DeleteCommand는 복구할 수 있도록 삭제한 내용을 기억하고, InsertCommand는 실행 취소 시 정확히 몇 개의 문자를 제거해야 하는지 알고 있습니다. 이것이 바로 커맨드 패턴의 힘입니다. 작업이 저장, 실행 및 취소될 수 있는 일급 객체(first-class objects)가 됩니다.
치트 시트
커맨드 패턴(Command pattern)은 요청을 객체로 캡슐화하여, 서로 다른 요청을 가진 클라이언트를 매개변수화하거나, 작업을 큐에 넣거나, 실행 취소(undo) 기능을 지원할 수 있게 합니다. 이는 작업을 호출하는 객체와 해당 작업을 수행하는 방법을 아는 객체를 분리합니다.
이 패턴은 네 가지 핵심 구성 요소를 가집니다:
execute()메서드를 포함하는 커맨드 인터페이스(Command interface)- 특정 작업을 구현하는 구체적인 커맨드(Concrete Commands)
- 실제 작업을 수행하는 수신자(Receiver)
- 커맨드를 트리거하는 호출자(Invoker)
기본 구조:
// Receiver - 작업을 수행하는 방법을 알고 있음
class Light {
public:
void turnOn() { std::cout << "Light is ON\n"; }
void turnOff() { std::cout << "Light is OFF\n"; }
};
// Command interface
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
// Concrete Command
class LightOnCommand : public Command {
Light& light;
public:
LightOnCommand(Light& l) : light(l) {}
void execute() override { light.turnOn(); }
void undo() override { light.turnOff(); }
};
// Invoker
class RemoteControl {
std::vector<std::unique_ptr<Command>> history;
public:
void pressButton(std::unique_ptr<Command> cmd) {
cmd->execute();
history.push_back(std::move(cmd));
}
void pressUndo() {
if (!history.empty()) {
history.back()->undo();
history.pop_back();
}
}
};각 커맨드는 수신자에 대한 참조를 저장하고 실행될 때 적절한 메서드를 호출합니다. 호출자는 어떤 작업이 일어날지 알지 못하며, 단지 execute()를 호출할 뿐입니다. 커맨드를 히스토리에 저장함으로써, 각 커맨드의 undo() 메서드를 호출하여 실행 취소를 구현할 수 있습니다.
작업을 큐에 넣어야 하거나, 실행 취소/재실행(undo/redo)을 구현해야 할 때, 또는 요청을 보내는 객체와 처리하는 객체를 분리하고 싶을 때 커맨드 패턴을 사용하세요.
직접 해보기
#include <iostream>
#include <string>
#include <memory>
#include "TextDocument.h"
#include "Command.h"
#include "TextEditor.h"
int main() {
// 입력 읽기
std::string firstText;
std::string secondText;
int deleteCount;
std::getline(std::cin, firstText);
std::getline(std::cin, secondText);
std::cin >> deleteCount;
// TODO: TextDocument와 TextEditor 생성
// TODO: 다음 작업을 순서대로 수행하고, 각 작업 후 내용을 표시:
// 1. 첫 번째 텍스트를 삽입한 후 내용 표시
// 2. 두 번째 텍스트를 삽입한 후 내용 표시
// 3. 지정된 개수만큼 문자를 삭제한 후 내용 표시
// 4. 한 번 실행 취소(undo)한 후 내용 표시
// 5. 한 번 더 실행 취소(undo)한 후 내용 표시
// 힌트: std::make_unique<InsertCommand>(...)와
// std::make_unique<DeleteCommand>(...)를 사용하여 명령 생성
return 0;
}
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.