C++ Command Pattern 완벽 가이드 | 실행 취소와 매크로 시스템

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. 기본 구조
  2. Undo/Redo 구현
  3. 매크로 시스템
  4. 트랜잭션
  5. 자주 발생하는 문제와 해결법
  6. 프로덕션 패턴
  7. 완전한 예제: 텍스트 에디터

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::asyncstd::thread로 비동기 실행 가능합니다.

Q5: Receiver 생명주기는?

A: shared_ptr로 관리하거나, Command가 Receiver보다 먼저 소멸되도록 보장하세요.

Q6: Command Pattern 학습 리소스는?

A:

한 줄 요약: Command Pattern으로 요청을 객체화하고 Undo/Redo를 구현할 수 있습니다. 다음으로 State Pattern을 읽어보면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ Observer Pattern 완벽 가이드 | 이벤트 기반 아키텍처와 신호/슬롯
  • C++ Strategy Pattern 완벽 가이드 | 알고리즘 캡슐화와 런타임 교체

관련 글

  • C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
  • C++ 컨테이너 어댑터 |
  • C++ CRTP 완벽 가이드 | 정적 다형성과 컴파일 타임 최적화
  • C++ Decorator Pattern 완벽 가이드 | 기능 동적 추가와 조합
  • C++ Factory Pattern 완벽 가이드 | 객체 생성 캡슐화와 확장성