Command Pattern
요청 자체를 Command 객체로 캡슐화해 Invoker가 Receiver를 모른 채 실행하는 패턴. 큐잉, 로그, 취소, 지연 실행이 자연스러워진다.
난이도 중급 · 선행 Strategy
한 줄 요약
“어떤 일을 한다”는 행위 자체를 객체로 만든다. 그러면 그 행위를 큐에 넣거나, 나중에 실행하거나, 거꾸로 되돌리는 일이 자연스럽게 풀린다.
어떤 문제를 푸는가
텍스트 에디터에 Undo 기능을 만든다. 단순하게 짜면 이런 식이 된다.
1
2
3
4
5
6
7
8
9
10
class Editor {
std::string text;
public:
void type(const std::string& s) { text += s; }
void deleteLast(int n) { text.erase(text.size() - n); }
void undo() {
// ??? 직전에 무슨 동작을 했는지 어떻게 알지?
// 동작별로 enum + 보조 데이터를 따로 관리해야 한다
}
};
문제:
- 동작별로 “되돌리는 법”이 다른데, Editor에 그걸 다 알게 하면 Editor가 비대해진다.
- 매크로(여러 동작 묶어 한 번에 실행)나 작업 큐를 만들려면 동작을 1급 시민으로 다룰 수단이 없다.
- 단축키와 메뉴 항목에 같은 동작을 바인딩하려면 결국 함수 포인터/람다로 가는데, 그러면 상태가 있는 동작이 표현되지 않는다.
패턴 적용 후
요청(동작)을 객체로 만든다. 객체는 execute()와 undo()를 안다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <iostream>
#include <memory>
#include <string>
#include <vector>
class Editor {
std::string text;
public:
void insert(const std::string& s) { text += s; }
void erase(size_t n) { text.erase(text.size() - n); }
const std::string& content() const { return text; }
};
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
class InsertCommand : public Command {
Editor& editor;
std::string payload;
public:
InsertCommand(Editor& editor, std::string s)
: editor(editor), payload(std::move(s)) {}
void execute() override { editor.insert(payload); }
void undo() override { editor.erase(payload.size()); }
};
class DeleteCommand : public Command {
Editor& editor;
std::string removed; // undo를 위해 지운 내용을 저장
size_t count;
public:
DeleteCommand(Editor& editor, size_t n) : editor(editor), count(n) {}
void execute() override {
const auto& t = editor.content();
removed = t.substr(t.size() - count);
editor.erase(count);
}
void undo() override { editor.insert(removed); }
};
class History {
std::vector<std::unique_ptr<Command>> stack;
public:
void run(std::unique_ptr<Command> cmd) {
cmd->execute();
stack.push_back(std::move(cmd));
}
void undo() {
if (stack.empty()) return;
stack.back()->undo();
stack.pop_back();
}
};
int main() {
Editor editor;
History history;
history.run(std::make_unique<InsertCommand>(editor, "Hello "));
history.run(std::make_unique<InsertCommand>(editor, "World"));
std::cout << editor.content() << std::endl; // Hello World
history.undo();
std::cout << editor.content() << std::endl; // Hello
history.run(std::make_unique<DeleteCommand>(editor, 6));
std::cout << editor.content() << std::endl; // (빈 문자열)
history.undo();
std::cout << editor.content() << std::endl; // Hello
}
달라진 점:
- 동작이 객체라서 스택·큐·리스트에 자유롭게 담는다.
- Undo는 마지막 객체의
undo()호출 한 줄. Editor는 Undo를 몰라도 된다. - 새 동작(서식, 자르기, 붙여넣기)은 새 Command 클래스로 추가. Editor 무손상.
구조
- Command:
execute()/undo()인터페이스 - ConcreteCommand: 실제 동작과 Receiver 호출 (
InsertCommand) - Invoker: Command를 실행하는 쪽 (
History) - Receiver: 실제 일을 하는 객체 (
Editor) - Client: Command를 만들어 Invoker에게 건네는 쪽
1
2
3
4
Client ──creates──▶ Command ──knows──▶ Receiver
▲
│
Invoker (실행, 큐, undo 스택)
Invoker는 Receiver를 모른다. 이게 핵심.
매크로 커맨드
여러 Command를 묶어 하나의 Command처럼 실행한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MacroCommand : public Command {
std::vector<std::unique_ptr<Command>> commands;
public:
void add(std::unique_ptr<Command> cmd) { commands.push_back(std::move(cmd)); }
void execute() override {
for (auto& c : commands) c->execute();
}
void undo() override {
// 역순으로 되돌리기
for (auto it = commands.rbegin(); it != commands.rend(); ++it) {
(*it)->undo();
}
}
};
Undo 순서가 역순인 이유: 동작 간 의존성이 있을 수 있다. A → B 순으로 실행했으면 B → A 순으로 되돌려야 안전하다.
실전 사례
- GUI 버튼·메뉴·단축키 바인딩: 같은 Command 객체를 여러 위젯에 묶는다.
- 작업 큐 / 스레드풀: 큐에 Command를 넣고 워커가 꺼내서
execute(). RabbitMQ·Celery 같은 메시지 큐가 같은 발상. - Redux의 Action: 객체 형태로 표현된 “할 일”. 실행 로직(reducer)과 분리됨.
- DB 트랜잭션 로그: 커밋된 Command들을 디스크에 적어두면, 크래시 후 재실행으로 복구 가능.
Strategy Pattern과의 차이
같은 “행위를 객체로” 발상이라 헷갈리기 쉽다. 의도가 다르다.
| Command | Strategy | |
|---|---|---|
| 의도 | 요청을 객체로 — 큐잉·로그·undo | 알고리즘을 교체 가능하게 |
| 핵심 메서드 | execute() (보통 인자 없음) | algorithm(input) (인자 받음) |
| Receiver | 객체 안에 들고 다님 | 없음 — Context가 호출 |
| 시점 | 생성 ≠ 실행 (지연·큐잉) | 생성 ≈ 곧 실행 |
Command는 “할 일 자체”를 캡슐화. Strategy는 “할 일의 방법”을 캡슐화.
안티패턴 / 주의
- 단순 함수 한 줄이면 Command 클래스 만들지 마라.
std::function<void()>한 줄이 더 깔끔하다. Command 클래스는 (1) undo가 필요하거나 (2) 상태를 들고 다녀야 하거나 (3) 동작을 직렬화해야 할 때 가치가 있다. - Receiver를 값으로 들고 있지 마라. 위 예제처럼 참조(또는 포인터)로 들어야 동작이 같은 Editor에 적용된다.
- Undo가 항상 가능한 건 아니다. “이메일 발송” 같은 부수효과는 되돌릴 수 없다. Undoable / Non-undoable을 구분해 둘 것.
- 메모리: undo 스택이 무한정 자라지 않게 상한을 둔다. 깊은 매크로의 메모리 누적도 주의.
스스로 점검
1. 텍스트 에디터에서 “복사 + 자르기 + 붙여넣기”를 매크로로 묶어 실행하려면?
답
MacroCommand — 여러 Command를 vector에 담아두고 execute()에서 차례로 호출. undo는 반드시 역순으로(B → A) 되돌려야 의존성이 깨지지 않는다.
2. “이메일 발송” 동작을 Command로 감싸면 undo가 가능할까?
답
아니다. 이미 전송된 이메일은 되돌릴 수 없는 부수효과. Undoable / Non-undoable을 구분해야 한다. 모든 동작이 undo 가능하다고 가정하면 잘못된 UX가 나온다.
3. std::function<void()> 한 줄과 Command 클래스, 언제 어느 쪽을 쓰나?
답
단순 콜백이면 std::function이 가볍다. Command 클래스는 (1) undo가 필요하거나 (2) 상태·메타데이터를 들고 다녀야 하거나 (3) 직렬화·로그 기록이 필요할 때 가치가 있다.