JAVA로 구현하는 알고리즘과 디자인 패턴 공부: 커맨드 패턴 (Command)
1. 커맨드 패턴이란? 📜
개념: 요청(Request)을 객체로 캡슐화하여, 요청이 실행되는 시점이나 방식, 수신자 등을 동적으로 결정할 수 있게 만드는 패턴입니다. 이 패턴을 사용하면 요청을 큐에 저장하거나, 로깅하거나, 되돌리는(Undo) 기능을 쉽게 구현할 수 있습니다.
레스토랑에서 주문하는 과정을 생각하면 쉽습니다.
- 손님 (Client): 요청을 생성합니다. ("스테이크 주세요")
- 웨이터 (Invoker): 요청을 받아 큐에 전달합니다. 주문서(Command)를 주방에 전달하는 역할을 합니다.
- 주문서 (Command): 요청에 필요한 모든 정보(메뉴: 스테이크, 수량: 1)를 담고 있는 객체입니다.
- 요리사 (Receiver): 실제 요청을 수행하는 객체입니다. 주문서를 보고 스테이크를 요리합니다.
여기서 핵심은 손님과 요리사가 서로를 전혀 모른다는 것입니다. 웨이터가 받은 '주문서(커맨드 객체)'가 이 둘을 완벽하게 분리시켜 줍니다.
2. 왜 사용할까요?
- 요청자와 수신자 분리: 요청을 보내는 객체(Invoker)와 요청을 실제로 처리하는 객체(Receiver)가 서로 직접적인 의존 관계를 갖지 않게 됩니다.
- Undo/Redo 기능 구현: 커맨드 객체에 execute() 메서드와 함께 undo() 메서드를 만들어 두면, 실행했던 명령을 스택에 쌓아두었다가 역순으로 되돌리는 기능을 쉽게 구현할 수 있습니다.
- 요청의 지연 및 큐잉: 커맨드 객체를 큐(Queue)에 저장해두었다가 나중에 순서대로 처리하는 작업(Job) 큐를 만들 수 있습니다.
- 요청의 로깅: 실행된 커맨드 객체들을 순서대로 기록하여 시스템 장애 시 복구 자료로 사용할 수 있습니다.
3. Java로 구현하는 방법
시나리오: 버튼 하나짜리 간단한 리모컨을 만들어 보겠습니다. 이 버튼은 조명(Light)을 켜는 명령을 담고 있습니다.
1단계: 수신자(Receiver) 클래스 구현
요청을 받아 실제로 특정 작업을 수행하는 객체입니다.
// 요청을 실제로 처리하는 객체: 조명
public class Light {
public void on() {
System.out.println("조명이 켜졌습니다.");
}
public void off() {
System.out.println("조명이 꺼졌습니다.");
}
}
2단계: 커맨드(Command) 인터페이스 정의
모든 커맨드 객체가 구현해야 할 공통 규약입니다. 보통 execute() 메서드 하나만 가집니다.
// 모든 커맨드 객체가 구현할 공통 인터페이스
public interface Command {
void execute();
}
3단계: 구체적인 커맨드(Concrete Command) 클래스 구현
이 패턴의 핵심입니다. 수신자(Receiver) 객체에 대한 참조를 가지고 있으며, execute() 메서드가 호출되면 해당 수신자의 특정 메서드를 실행합니다.
// 구체적인 커맨드: 조명을 켜는 명령
public class LightOnCommand implements Command {
private Light light; // 이 명령을 수행할 수신자 객체
public LightOnCommand(Light light) {
this.light = light;
}
// execute() 메서드는 수신자의 해당 작업을 호출한다.
@Override
public void execute() {
light.on();
}
}
4단계: 호출자(Invoker) 클래스 구현
커맨드 객체를 받아서 execute()를 호출하는 역할을 합니다. 여기서는 리모컨이 해당됩니다.
// 요청을 호출하는 객체: 리모컨
public class SimpleRemoteControl {
private Command slot; // 하나의 커맨드 객체를 저장할 슬롯
public void setCommand(Command command) {
this.slot = command;
}
// 버튼을 누르면, 슬롯에 저장된 커맨드의 execute() 메서드를 호출한다.
// 리모컨은 자신이 어떤 장치(Light)를 제어하는지, 무슨 작업(on)을 하는지 전혀 모른다.
public void buttonWasPressed() {
slot.execute();
}
}
5단계: 클라이언트 코드에서 사용하기
클라이언트는 수신자, 커맨드, 호출자를 모두 생성하고 연결해주는 역할을 합니다.
public class RemoteLoader {
public static void main(String[] args) {
// 1. 호출자(리모컨) 생성
SimpleRemoteControl remote = new SimpleRemoteControl();
// 2. 수신자(조명) 생성
Light light = new Light();
// 3. 커맨드 객체 생성 (수신자를 전달)
Command lightOn = new LightOnCommand(light);
// 4. 리모컨에 커맨드 객체를 설정
remote.setCommand(lightOn);
// 5. 버튼을 누른다.
remote.buttonWasPressed(); // "조명이 켜졌습니다." 출력
}
}
4. 커맨드 패턴과 실제 사례
- GUI 버튼 및 메뉴 항목: GUI 애플리케이션에서 메뉴 항목을 클릭하거나 버튼을 누를 때, 각 항목은 특정 동작을 수행하는 Command 객체에 연결될 수 있습니다. 이를 통해 UI 요소와 실제 비즈니스 로직을 분리할 수 있습니다.
- 스레드 풀과 작업 큐: Runnable 인터페이스는 커맨드 패턴의 한 형태입니다. Runnable 객체는 run()이라는 메서드 하나만 가진 커맨드 객체이며, 이를 스레드 풀의 작업 큐에 넣어두면 여러 스레드가 순차적으로 작업을 처리하게 됩니다.
- 트랜잭션 관리: 데이터베이스의 여러 작업을 하나의 트랜잭션으로 묶어 처리할 때, 각 작업을 커맨드 객체로 만들고 순서대로 실행하다가 문제가 생기면 undo()를 호출하여 롤백하는 로직을 구현할 수 있습니다.
5. 문제 제시 및 답변 💡
문제: 텍스트 에디터에서 '복사(Copy)', '붙여넣기(Paste)' 기능을 구현하려고 합니다. 사용자가 메뉴에서 '복사'를 선택하면, 현재 선택된 텍스트가 클립보드에 저장되어야 합니다. 커맨드 패턴을 사용하여 이 기능을 어떻게 설계할 수 있을까요?
답변:
- Editor 클래스 (Receiver): 실제 텍스트 편집 작업을 수행하는 수신자입니다. 현재 선택된 텍스트를 반환하는 getSelection() 메서드와 클립보드 내용을 설정하는 setClipboard() 같은 메서드를 가집니다.
- Command 인터페이스: execute() 메서드를 가진 공통 인터페이스입니다.
- CopyCommand 클래스 (Concrete Command): Command 인터페이스를 구현합니다. 생성자에서 Editor 객체를 주입받습니다. execute() 메서드가 호출되면, 내부의 Editor 객체로부터 getSelection()으로 선택된 텍스트를 가져오고, setClipboard()로 클립보드에 저장하는 로직을 수행합니다.
- MenuItem 클래스 (Invoker): 메뉴 항목 역할을 하는 호출자입니다. CopyCommand 객체를 가지고 있다가, click() 메서드가 호출되면 CopyCommand의 execute()를 호출합니다.
이렇게 설계하면, MenuItem은 자신이 '복사' 기능을 수행한다는 사실만 알 뿐, 실제 텍스트를 어떻게 가져오고 클립보드에 어떻게 저장하는지에 대한 복잡한 로직을 알 필요가 없어집니다.
'프로그래밍 > 알고리즘&자료구조&패턴' 카테고리의 다른 글
| 디자인패턴 - 상태 패턴 (1) | 2025.08.18 |
|---|---|
| 디자인패턴 - 템플릿 매서드 패턴 (2) | 2025.08.18 |
| 디자인패턴 - 옵저버 패턴 (2) | 2025.08.18 |
| 디자인패턴 - 전략패턴 (3) | 2025.08.18 |
| 디자인패턴 - 컴포지트 패턴 (2) | 2025.08.18 |