요청 자체를 객체로 만드는 패턴. 실행을 미루거나 기록하거나 되돌릴 수 있게 된다.
interface Command {
void execute();
void undo();
}
class CopyCommand implements Command {
private final Editor editor;
private String backup;
public void execute() { backup = editor.getText(); editor.copy(); }
public void undo() { editor.setText(backup); }
}
// 실행 이력을 스택에 쌓으면 실행 취소가 된다
Deque<Command> history = new ArrayDeque<>();
void run(Command c) { c.execute(); history.push(c); }
void undo() { if (!history.isEmpty()) history.pop().undo(); }
무엇을 가능하게 하나
- 실행 취소 / 다시 실행
- 요청을 큐에 넣어 나중에 실행 (작업 큐)
- 요청을 로그로 남겨 재실행 (장애 복구)
- 매크로 — 커맨드를 묶어 하나처럼 실행 (컴포지트와 결합)
- 호출자와 수신자를 분리 (버튼이 무엇이 실행될지 몰라도 된다)
// 같은 커맨드를 버튼·단축키·메뉴가 공유한다
button.setCommand(new CopyCommand(editor));
shortcut.setCommand(new CopyCommand(editor));
Memento와 짝을 이룬다
// 상태 전체를 스냅샷으로 저장했다 복원한다
class Editor {
Memento save() { return new Memento(text, cursor); }
void restore(Memento m) { this.text = m.text; this.cursor = m.cursor; }
}
Command 의 undo 는 '역연산' 을 구현한다 (되돌리는 방법을 안다) Memento 는 '이전 상태' 를 통째로 저장한다 (되돌리는 방법을 몰라도 된다)
역연산을 정의하기 어려우면 Memento 가 낫다 (대신 메모리를 더 쓴다)
이벤트 소싱과의 관계
상태를 저장하는 대신 '일어난 명령/사건' 을 순서대로 저장한다
→ 처음부터 재생하면 현재 상태가 나온다
→ 과거 어느 시점으로도 되돌릴 수 있다
→ Command 를 영속화한 것과 같은 발상이다
비용
- 명령 하나마다 클래스가 하나씩 는다
- 단순한 호출을 객체로 감싸는 간접층이 생긴다
실행 취소 · 큐잉 · 로깅 중 하나라도 필요할 때 값을 한다 그냥 메서드를 부르면 되는 곳에 쓰면 순수한 복잡도다
면접 함정
- ❌ "Command는 실행 취소를 위한 것" → 그건 응용 중 하나다. 본질은 요청의 객체화다.
- ❌ "람다가 있으니 Command는 불필요" → 실행 취소·직렬화·이력이 필요하면 여전히 객체여야 한다.
매크로 — 컴포지트와 결합
class MacroCommand implements Command {
private final List<Command> commands;
public void execute() { commands.forEach(Command::execute); }
public void undo() {
// 역순으로 되돌린다 — 순서가 중요하다
for (int i = commands.size() - 1; i >= 0; i--) commands.get(i).undo();
}
}
되돌릴 수 없는 명령이 섞이면
-
메일 발송 · 결제 승인처럼 되돌릴 수 없는 명령이 있다
-
① 그런 명령은 undo() 에서 예외를 던지게 하고 UI 에서 막는다
-
② 보상 동작을 정의한다 (결제 취소 · 정정 메일)
-
③ 되돌릴 수 없는 명령을 맨 뒤에 배치한다
-
셋 중 무엇을 택할지는 업무가 정한다
큐잉과 직렬화
// 커맨드를 직렬화하면 프로세스를 넘어 실행할 수 있다
queue.send(new ProcessOrderCommand(orderId));
// 이 시점부터 커맨드는 '메시지' 와 사실상 같은 것이 된다
// → 메시지 큐 기반 아키텍처가 이 패턴의 분산 버전이다