C++ Command Pattern 완벽 가이드 | 실행 취소와 매크로 시스템
이 글의 핵심
C++ Command Pattern 완벽 가이드에 대한 실전 가이드입니다. 실행 취소와 매크로 시스템 등을 예제와 함께 상세히 설명합니다.
Command Pattern이란? 왜 필요한가
문제 시나리오: 실행 취소 구현
문제: 텍스트 에디터에서 Undo/Redo를 구현하려면, 모든 작업을 기록하고 역순으로 실행해야 합니다. 작업을 함수 호출로만 하면 기록이 어렵습니다.
// 나쁜 예: 함수 호출만
void insertText(std::string& doc, const std::string& text) {
doc += text;
// Undo를 어떻게?
}
해결: Command Pattern은 요청을 객체로 캡슐화합니다. 각 Command는 execute()와 undo()를 가지며, 히스토리 스택에 저장됩니다.
// 좋은 예: Command 객체
class InsertCommand : public Command {
public:
InsertCommand(std::string& doc, const std::string& text)
: doc_(doc), text_(text), position_(doc.size()) {}
void execute() override {
doc_ += text_;
}
void undo() override {
doc_.erase(position_, text_.size());
}
private:
std::string& doc_;
std::string text_;
size_t position_;
};
flowchart TD
invoker["Invoker (Editor)"]
cmd["Command"]
insert["InsertCommand"]
delete["DeleteCommand"]
receiver["Receiver (Document)"]
invoker -->|execute| cmd
cmd <|-- insert
cmd <|-- delete
insert --> receiver
delete --> receiver
목차
1. 기본 구조
최소 Command
#include <iostream>
#include <memory>
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
class Light {
public:
void on() { std::cout << "Light ON\n"; }
void off() { std::cout << "Light OFF\n"; }
};
class LightOnCommand : public Command {
public:
LightOnCommand(Light& light) : light_(light) {}
void execute() override { light_.on(); }
void undo() override { light_.off(); }
private:
Light& light_;
};
class LightOffCommand : public Command {
public:
LightOffCommand(Light& light) : light_(light) {}
void execute() override { light_.off(); }
void undo() override { light_.on(); }
private:
Light& light_;
};
class RemoteControl {
public:
void setCommand(std::unique_ptr<Command> cmd) {
command_ = std::move(cmd);
}
void pressButton() {
if (command_) {
command_->execute();
}
}
void pressUndo() {
if (command_) {
command_->undo();
}
}
private:
std::unique_ptr<Command> command_;
};
int main() {
Light light;
RemoteControl remote;
remote.setCommand(std::make_unique<LightOnCommand>(light));
remote.pressButton(); // Light ON
remote.pressUndo(); // Light OFF
}
2. Undo/Redo 구현
히스토리 스택
#include <iostream>
#include <memory>
#include <stack>
#include <string>
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
class Document {
public:
void insert(const std::string& text) {
content_ += text;
std::cout << "Document: " << content_ << '\n';
}
void remove(size_t pos, size_t len) {
content_.erase(pos, len);
std::cout << "Document: " << content_ << '\n';
}
const std::string& getContent() const { return content_; }
private:
std::string content_;
};
class InsertCommand : public Command {
public:
InsertCommand(Document& doc, const std::string& text)
: doc_(doc), text_(text), position_(doc.getContent().size()) {}
void execute() override {
doc_.insert(text_);
}
void undo() override {
doc_.remove(position_, text_.size());
}
private:
Document& doc_;
std::string text_;
size_t position_;
};
class CommandManager {
public:
void executeCommand(std::unique_ptr<Command> cmd) {
cmd->execute();
undoStack_.push(std::move(cmd));
// Redo 스택 클리어
while (!redoStack_.empty()) {
redoStack_.pop();
}
}
void undo() {
if (!undoStack_.empty()) {
auto cmd = std::move(undoStack_.top());
undoStack_.pop();
cmd->undo();
redoStack_.push(std::move(cmd));
}
}
void redo() {
if (!redoStack_.empty()) {
auto cmd = std::move(redoStack_.top());
redoStack_.pop();
cmd->execute();
undoStack_.push(std::move(cmd));
}
}
private:
std::stack<std::unique_ptr<Command>> undoStack_;
std::stack<std::unique_ptr<Command>> redoStack_;
};
int main() {
Document doc;
CommandManager manager;
manager.executeCommand(std::make_unique<InsertCommand>(doc, "Hello "));
manager.executeCommand(std::make_unique<InsertCommand>(doc, "World"));
manager.undo(); // "Hello "
manager.undo(); // ""
manager.redo(); // "Hello "
manager.redo(); // "Hello World"
}
3. 매크로 시스템
복합 Command
#include <iostream>
#include <memory>
#include <vector>
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
class MacroCommand : public Command {
public:
void add(std::unique_ptr<Command> cmd) {
commands_.push_back(std::move(cmd));
}
void execute() override {
for (auto& cmd : commands_) {
cmd->execute();
}
}
void undo() override {
// 역순으로 undo
for (auto it = commands_.rbegin(); it != commands_.rend(); ++it) {
(*it)->undo();
}
}
private:
std::vector<std::unique_ptr<Command>> commands_;
};
class PrintCommand : public Command {
public:
PrintCommand(const std::string& msg) : message_(msg) {}
void execute() override {
std::cout << message_ << '\n';
}
void undo() override {
std::cout << "Undo: " << message_ << '\n';
}
private:
std::string message_;
};
int main() {
auto macro = std::make_unique<MacroCommand>();
macro->add(std::make_unique<PrintCommand>("Step 1"));
macro->add(std::make_unique<PrintCommand>("Step 2"));
macro->add(std::make_unique<PrintCommand>("Step 3"));
macro->execute();
// Step 1
// Step 2
// Step 3
macro->undo();
// Undo: Step 3
// Undo: Step 2
// Undo: Step 1
}
4. 트랜잭션
All-or-Nothing
#include <iostream>
#include <memory>
#include <vector>
#include <stdexcept>
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
class Transaction {
public:
void add(std::unique_ptr<Command> cmd) {
commands_.push_back(std::move(cmd));
}
bool commit() {
try {
for (auto& cmd : commands_) {
cmd->execute();
}
return true;
} catch (const std::exception& e) {
std::cerr << "Transaction failed: " << e.what() << '\n';
rollback();
return false;
}
}
void rollback() {
for (auto it = commands_.rbegin(); it != commands_.rend(); ++it) {
try {
(*it)->undo();
} catch (...) {
// Rollback 실패는 무시
}
}
}
private:
std::vector<std::unique_ptr<Command>> commands_;
};
class Account {
public:
Account(double balance) : balance_(balance) {}
void deposit(double amount) {
balance_ += amount;
std::cout << "Deposited $" << amount << ", Balance: $" << balance_ << '\n';
}
void withdraw(double amount) {
if (balance_ < amount) {
throw std::runtime_error("Insufficient funds");
}
balance_ -= amount;
std::cout << "Withdrew $" << amount << ", Balance: $" << balance_ << '\n';
}
private:
double balance_;
};
class DepositCommand : public Command {
public:
DepositCommand(Account& acc, double amount) : account_(acc), amount_(amount) {}
void execute() override { account_.deposit(amount_); }
void undo() override { account_.withdraw(amount_); }
private:
Account& account_;
double amount_;
};
class WithdrawCommand : public Command {
public:
WithdrawCommand(Account& acc, double amount) : account_(acc), amount_(amount) {}
void execute() override { account_.withdraw(amount_); }
void undo() override { account_.deposit(amount_); }
private:
Account& account_;
double amount_;
};
int main() {
Account acc(100.0);
Transaction txn;
txn.add(std::make_unique<WithdrawCommand>(acc, 50.0));
txn.add(std::make_unique<DepositCommand>(acc, 30.0));
txn.add(std::make_unique<WithdrawCommand>(acc, 100.0)); // 실패
if (!txn.commit()) {
std::cout << "Transaction rolled back\n";
}
}
5. 자주 발생하는 문제와 해결법
문제 1: Receiver 생명주기
증상: Dangling reference.
원인: Command가 Receiver를 참조하는데, Receiver가 먼저 소멸.
// ❌ 잘못된 사용: 참조
class Command {
Receiver& receiver_; // Dangling 가능
};
// ✅ 올바른 사용: shared_ptr
class Command {
std::shared_ptr<Receiver> receiver_;
};
문제 2: Undo 불가능한 Command
증상: Undo 시 복원 불가.
원인: 상태를 저장하지 않았습니다.
// ❌ 잘못된 사용: 상태 미저장
class DeleteCommand : public Command {
void undo() override {
// 삭제된 데이터를 어떻게 복원?
}
};
// ✅ 올바른 사용: 상태 저장
class DeleteCommand : public Command {
std::string deletedText_; // 저장
void execute() override {
deletedText_ = doc_.getText();
doc_.clear();
}
void undo() override {
doc_.setText(deletedText_);
}
};
6. 프로덕션 패턴
패턴 1: 히스토리 제한
class CommandManager {
public:
CommandManager(size_t maxHistory = 100) : maxHistory_(maxHistory) {}
void executeCommand(std::unique_ptr<Command> cmd) {
cmd->execute();
undoStack_.push(std::move(cmd));
// 히스토리 제한
if (undoStack_.size() > maxHistory_) {
undoStack_.pop();
}
while (!redoStack_.empty()) {
redoStack_.pop();
}
}
private:
size_t maxHistory_;
std::stack<std::unique_ptr<Command>> undoStack_;
std::stack<std::unique_ptr<Command>> redoStack_;
};
패턴 2: 비동기 Command
#include <future>
class AsyncCommand : public Command {
public:
void execute() override {
future_ = std::async(std::launch::async, [this]() {
// 비동기 작업
});
}
void wait() {
if (future_.valid()) {
future_.wait();
}
}
private:
std::future<void> future_;
};
7. 완전한 예제: 텍스트 에디터
#include <iostream>
#include <memory>
#include <stack>
#include <string>
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual std::string describe() const = 0;
virtual ~Command() = default;
};
class TextEditor {
public:
void insert(size_t pos, const std::string& text) {
content_.insert(pos, text);
}
void erase(size_t pos, size_t len) {
content_.erase(pos, len);
}
std::string getText(size_t pos, size_t len) const {
return content_.substr(pos, len);
}
const std::string& getContent() const { return content_; }
void print() const {
std::cout << "Content: \"" << content_ << "\"\n";
}
private:
std::string content_;
};
class InsertCommand : public Command {
public:
InsertCommand(TextEditor& editor, size_t pos, const std::string& text)
: editor_(editor), position_(pos), text_(text) {}
void execute() override {
editor_.insert(position_, text_);
}
void undo() override {
editor_.erase(position_, text_.size());
}
std::string describe() const override {
return "Insert \"" + text_ + "\" at " + std::to_string(position_);
}
private:
TextEditor& editor_;
size_t position_;
std::string text_;
};
class DeleteCommand : public Command {
public:
DeleteCommand(TextEditor& editor, size_t pos, size_t len)
: editor_(editor), position_(pos), length_(len) {}
void execute() override {
deletedText_ = editor_.getText(position_, length_);
editor_.erase(position_, length_);
}
void undo() override {
editor_.insert(position_, deletedText_);
}
std::string describe() const override {
return "Delete " + std::to_string(length_) + " chars at " + std::to_string(position_);
}
private:
TextEditor& editor_;
size_t position_;
size_t length_;
std::string deletedText_;
};
class EditorController {
public:
EditorController(TextEditor& editor) : editor_(editor) {}
void execute(std::unique_ptr<Command> cmd) {
std::cout << "Executing: " << cmd->describe() << '\n';
cmd->execute();
editor_.print();
undoStack_.push(std::move(cmd));
while (!redoStack_.empty()) {
redoStack_.pop();
}
}
void undo() {
if (undoStack_.empty()) {
std::cout << "Nothing to undo\n";
return;
}
auto cmd = std::move(undoStack_.top());
undoStack_.pop();
std::cout << "Undoing: " << cmd->describe() << '\n';
cmd->undo();
editor_.print();
redoStack_.push(std::move(cmd));
}
void redo() {
if (redoStack_.empty()) {
std::cout << "Nothing to redo\n";
return;
}
auto cmd = std::move(redoStack_.top());
redoStack_.pop();
std::cout << "Redoing: " << cmd->describe() << '\n';
cmd->execute();
editor_.print();
undoStack_.push(std::move(cmd));
}
private:
TextEditor& editor_;
std::stack<std::unique_ptr<Command>> undoStack_;
std::stack<std::unique_ptr<Command>> redoStack_;
};
int main() {
TextEditor editor;
EditorController controller(editor);
controller.execute(std::make_unique<InsertCommand>(editor, 0, "Hello"));
controller.execute(std::make_unique<InsertCommand>(editor, 5, " World"));
controller.execute(std::make_unique<DeleteCommand>(editor, 5, 6));
controller.undo();
controller.undo();
controller.redo();
}
정리
| 개념 | 설명 |
|---|---|
| Command Pattern | 요청을 객체로 캡슐화 |
| 목적 | Undo/Redo, 매크로, 트랜잭션, 큐 |
| 구조 | Command, Invoker, Receiver |
| 장점 | 요청 기록, 취소 가능, 조합 가능 |
| 단점 | 클래스 증가, 메모리 사용 |
| 사용 사례 | 에디터, GUI, 트랜잭션, 작업 큐 |
Command Pattern은 요청을 객체화해 Undo/Redo와 매크로를 구현하는 강력한 패턴입니다.
FAQ
Q1: Command Pattern은 언제 쓰나요?
A: Undo/Redo, 매크로, 트랜잭션, 작업 큐가 필요할 때 사용합니다.
Q2: Memento Pattern과 차이는?
A: Command는 작업 기록, Memento는 상태 스냅샷에 집중합니다.
Q3: 메모리 사용량은?
A: 히스토리 스택이 커지면 메모리가 증가합니다. 히스토리 제한을 두세요.
Q4: 비동기 Command는?
A: std::async나 std::thread로 비동기 실행 가능합니다.
Q5: Receiver 생명주기는?
A: shared_ptr로 관리하거나, Command가 Receiver보다 먼저 소멸되도록 보장하세요.
Q6: Command Pattern 학습 리소스는?
A:
- “Design Patterns” by Gang of Four
- “Head First Design Patterns” by Freeman & Freeman
- Refactoring Guru: Command Pattern
한 줄 요약: Command Pattern으로 요청을 객체화하고 Undo/Redo를 구현할 수 있습니다. 다음으로 State Pattern을 읽어보면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Observer Pattern 완벽 가이드 | 이벤트 기반 아키텍처와 신호/슬롯
- C++ Strategy Pattern 완벽 가이드 | 알고리즘 캡슐화와 런타임 교체
관련 글
- C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
- C++ 컨테이너 어댑터 |
- C++ CRTP 완벽 가이드 | 정적 다형성과 컴파일 타임 최적화
- C++ Decorator Pattern 완벽 가이드 | 기능 동적 추가와 조합
- C++ Factory Pattern 완벽 가이드 | 객체 생성 캡슐화와 확장성