C++ Command 패턴

요청을 객체로 캡슐화하여 실행 취소, 재실행, 큐잉이 가능한 커맨드 시스템입니다. 텍스트 에디터의 편집 히스토리 관리를 예시로 구현합니다.

Gist
#include <iostream>
#include <memory>
#include <stack>
#include <string>
#include <vector>
#include <stdexcept>

// ── 커맨드 인터페이스 ─────────────────────────────────────────────────────────
class Command {
public:
    virtual ~Command() = default;
    virtual void execute() = 0;
    virtual void undo()    = 0;
    virtual std::string description() const = 0;
};

// ── 수신자(Receiver): 텍스트 문서 ─────────────────────────────────────────────
class Document {
public:
    void insertAt(std::size_t pos, const std::string& text) {
        if (pos > content_.size())
            throw std::out_of_range("삽입 위치가 범위를 벗어났습니다.");
        content_.insert(pos, text);
    }

    void eraseAt(std::size_t pos, std::size_t len) {
        if (pos + len > content_.size())
            throw std::out_of_range("삭제 범위가 범위를 벗어났습니다.");
        content_.erase(pos, len);
    }

    std::string getContent() const { return content_; }
    std::size_t size()       const { return content_.size(); }

private:
    std::string content_;
};

// ── 구체 커맨드: 텍스트 삽입 ─────────────────────────────────────────────────
class InsertCommand final : public Command {
public:
    InsertCommand(Document& doc, std::size_t pos, std::string text)
        : doc_(doc), pos_(pos), text_(std::move(text)) {}

    void execute() override { doc_.insertAt(pos_, text_); }
    void undo()    override { doc_.eraseAt(pos_, text_.size()); }

    std::string description() const override {
        return "삽입(pos=" + std::to_string(pos_) + ", \"" + text_ + "\")";
    }

private:
    Document&   doc_;
    std::size_t pos_;
    std::string text_;
};

// ── 구체 커맨드: 텍스트 삭제 ─────────────────────────────────────────────────
class EraseCommand final : public Command {
public:
    EraseCommand(Document& doc, std::size_t pos, std::size_t len)
        : doc_(doc), pos_(pos), len_(len) {}

    void execute() override {
        // 실행 전 삭제될 텍스트를 저장해 undo 에 사용
        saved_ = doc_.getContent().substr(pos_, len_);
        doc_.eraseAt(pos_, len_);
    }

    void undo() override { doc_.insertAt(pos_, saved_); }

    std::string description() const override {
        return "삭제(pos=" + std::to_string(pos_)
               + ", len=" + std::to_string(len_) + ")";
    }

private:
    Document&   doc_;
    std::size_t pos_;
    std::size_t len_;
    std::string saved_; // undo 를 위해 보관
};

// ── 매크로 커맨드: 여러 커맨드를 하나로 묶음 ─────────────────────────────────
class MacroCommand final : public Command {
public:
    void add(std::shared_ptr<Command> cmd) {
        commands_.push_back(std::move(cmd));
    }

    void execute() override {
        for (auto& cmd : commands_) cmd->execute();
    }

    void undo() override {
        // 역순으로 취소
        for (auto it = commands_.rbegin(); it != commands_.rend(); ++it)
            (*it)->undo();
    }

    std::string description() const override {
        return "매크로(" + std::to_string(commands_.size()) + "개 커맨드)";
    }

private:
    std::vector<std::shared_ptr<Command>> commands_;
};

// ── 인보커: 커맨드 히스토리 & 실행/취소 관리 ─────────────────────────────────
class CommandHistory {
public:
    void execute(std::shared_ptr<Command> cmd) {
        cmd->execute();
        std::cout << "[실행] " << cmd->description() << '\n';
        undoStack_.push(std::move(cmd));
        // 새 커맨드 실행 시 redo 스택 초기화
        while (!redoStack_.empty()) redoStack_.pop();
    }

    void undo() {
        if (undoStack_.empty()) {
            std::cout << "[취소] 취소할 작업이 없습니다.\n";
            return;
        }
        auto cmd = undoStack_.top(); undoStack_.pop();
        cmd->undo();
        std::cout << "[취소] " << cmd->description() << '\n';
        redoStack_.push(std::move(cmd));
    }

    void redo() {
        if (redoStack_.empty()) {
            std::cout << "[재실행] 재실행할 작업이 없습니다.\n";
            return;
        }
        auto cmd = redoStack_.top(); redoStack_.pop();
        cmd->execute();
        std::cout << "[재실행] " << cmd->description() << '\n';
        undoStack_.push(std::move(cmd));
    }

private:
    std::stack<std::shared_ptr<Command>> undoStack_;
    std::stack<std::shared_ptr<Command>> redoStack_;
};

// ── 사용 예시 ─────────────────────────────────────────────────────────────────
int main() {
    Document doc;
    CommandHistory history;

    auto print = [&] {
        std::cout << "  문서: \"" << doc.getContent() << "\"\n";
    };

    // 텍스트 삽입
    history.execute(std::make_shared<InsertCommand>(doc, 0, "Hello"));
    print();

    history.execute(std::make_shared<InsertCommand>(doc, 5, ", World!"));
    print();

    // 텍스트 삭제
    history.execute(std::make_shared<EraseCommand>(doc, 5, 7)); // ", World" 삭제
    print();

    // 매크로 커맨드: 여러 편집을 하나의 원자 단위로
    auto macro = std::make_shared<MacroCommand>();
    macro->add(std::make_shared<InsertCommand>(doc, 5, " C++"));
    macro->add(std::make_shared<InsertCommand>(doc, 9, "17"));
    history.execute(macro);
    print();

    // 취소 (undo) 3회
    std::cout << "\n--- 취소 ---\n";
    history.undo(); print();
    history.undo(); print();
    history.undo(); print();

    // 재실행 (redo) 2회
    std::cout << "\n--- 재실행 ---\n";
    history.redo(); print();
    history.redo(); print();
}