"스마트 홈 리모컨을 만드는데, 1번 버튼은 전등을 켜고, 2번 버튼은 TV를 켭니다. 나중에는 버튼 설정을 바꾸고 싶어요."
버튼(Invoker)과 실제 기기(Receiver)가 강하게 결합되어 있으면, 기능을 바꿀 때마다 리모컨 코드를 뜯어고쳐야 합니다. 이를 해결하기 위해 **"명령(요청) 자체를 객체로 포장해서 전달"**하는 것이 커맨드 패턴입니다.

1. 방식 1: 클래식 커맨드 (Classic Command) - "Stateful & Undo"
GoF 디자인 패턴의 정석입니다. 명령을 단순한 실행이 아니라 **"기록 가능한 객체"**로 봅니다. 실행했던 명령을 스택(Stack)에 쌓아두면, 역순으로 꺼내서 **실행 취소(Undo)**를 구현할 수 있습니다.
특징
- execute()뿐만 아니라 undo() 메서드도 정의합니다.
- 명령 객체가 실행 당시의 상태(State)를 기억해야 할 수도 있습니다.
코드 예시
// 1. 커맨드 인터페이스
interface Command {
void execute();
void undo(); // 핵심 기능
}
// 2. 구체적 명령 (전등 켜기)
class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) { this.light = light; }
@Override
public void execute() {
light.on(); // 실제 동작
}
@Override
public void undo() {
light.off(); // 되돌리기 동작
}
}
// 3. 리모컨 (Invoker)
class RemoteControl {
private Command command; // 무엇을 실행할지 모름, 그냥 명령만 받음
private Stack<Command> history = new Stack<>(); // 실행 내역 저장
public void setCommand(Command command) { this.command = command; }
public void pressButton() {
command.execute();
history.push(command); // 기록
}
public void pressUndo() {
if (!history.isEmpty()) {
history.pop().undo(); // Ctrl+Z
}
}
}
장단점
- 장점: undo/redo 기능을 완벽하게 구현할 수 있습니다. 로그를 남겨서 시스템 장애 시 복구(Replay)하는 데에도 쓰입니다.
- 단점: 명령 하나마다 클래스를 하나씩 만들어야 하므로 클래스 개수가 폭발적으로 늘어납니다.
2. 방식 2: 심플 커맨드 (Functional Command) - "Stateless & Lambda"
현대적인 자바(Java 8+) 개발에서는 undo가 필요 없는 경우, 굳이 무거운 클래스를 만들지 않습니다. **"메서드 하나만 있는 인터페이스는 람다로 대체한다"**는 원칙을 적용합니다.
특징
- 별도의 Command 클래스 파일 없이, Runnable 인터페이스나 람다 표현식을 사용합니다.
- 자바의 스레드 풀(ExecutorService)이 일(Job)을 처리하는 방식과 동일합니다.
코드 예시
// 1. 리모컨 (Invoker)
class SimpleRemote {
// Command 인터페이스 대신 Runnable 사용 가능 (void run()과 구조 동일)
private Runnable command;
public void setCommand(Runnable command) { this.command = command; }
public void pressButton() {
command.run(); // 실행만 함 (Undo는 고려 안 함)
}
}
// 2. 사용 (Client)
public class Client {
public static void main(String[] args) {
Light light = new Light();
SimpleRemote remote = new SimpleRemote();
// 클래스 생성 없이 람다로 즉석에서 명령 주입
remote.setCommand(() -> light.on());
remote.pressButton();
remote.setCommand(() -> System.out.println("TV 켜기"));
remote.pressButton();
}
}
장단점
- 장점: 코드가 획기적으로 줄어듭니다. 작업 큐(Queue)나 스레드 풀에 작업을 던질 때 가장 많이 쓰는 패턴입니다.
- 단점: 상태를 저장하지 않으므로 undo 기능을 구현하기 어렵습니다.
3. 실무 예시: 어디서 쓰이나요?
1) Java 스레드 풀 (ExecutorService)
자바 개발자가 매일 쓰는 스레드 풀이 바로 커맨드 패턴의 거대한 소비자입니다.
ExecutorService pool = Executors.newFixedThreadPool(10);
// "이 작업을 실행해줘"라고 명령 객체(Runnable)를 큐에 넣음 -> 스레드가 꺼내서 실행
pool.submit(() -> System.out.println("Async Job"));
2) CQRS 패턴 (확장판)
최근 마이크로서비스 아키텍처에서 유행하는 **CQRS(Command Query Responsibility Segregation)**는 커맨드 패턴을 아키텍처 레벨로 확장한 것입니다.
- Command: 상태를 변경하는 명령 (Create, Update, Delete)
- Query: 데이터를 조회하는 명령 (Read)
- 이 둘을 철저히 분리하여 처리 성능을 최적화합니다.
요약: Undo가 필요한가?
| 구분 | Classic Command (GoF) | Functional Command (Lambda) |
| 구조 | implements Command 클래스 생성 | Runnable 또는 람다식 사용 |
| 핵심 기능 | 실행(execute) + 취소(undo) | 실행(run) only |
| 상태 저장 | 가능 (멤버 변수에 이전 상태 저장) | 불가능 (일회성 실행) |
| 추천 상황 | 텍스트 에디터, 그리기 도구, 트랜잭션 롤백 | 비동기 작업 처리, 이벤트 리스너, 버튼 클릭 |
결론
단순히 **"버튼을 눌렀을 때 실행할 로직을 갈아끼우고 싶다"**거나 **"비동기 작업을 큐에 쌓아두고 싶다"**면 **람다 방식(Functional)**이 정답입니다.
하지만 **"사용자가 실행 취소(Ctrl+Z)를 원한다"**거나 **"명령 수행 이력을 DB에 저장해서 나중에 재생해야 한다"**면, 반드시 정석적인 클래식 커맨드 패턴을 사용해야 합니다.
'프로그래밍 > 디자인패턴' 카테고리의 다른 글
| 데코레이터 패턴(Decorator Pattern) 총정리 (0) | 2025.12.07 |
|---|---|
| 프로토타입 패턴(Prototype Pattern) 완벽 정리 (0) | 2025.12.07 |
| 책임 연쇄 패턴(Chain of Responsibility) 총정리 (0) | 2025.12.07 |
| 이터레이터 패턴(Iterator Pattern) 완벽 정리 (0) | 2025.12.07 |
| 상태 패턴(State Pattern) 완벽 정리 (0) | 2025.12.07 |