"요청을 객체로 캡슐화하여 실행을 지연하거나 취소하자"
// 문제 1: 요청과 실행이 강하게 결합
public class TextEditor {
public void saveButton() {
save(); // 버튼이 직접 실행
}
public void copyButton() {
copy(); // 버튼이 직접 실행
}
// 단축키도 추가하려면? 메뉴도?
// 모든 곳에 save() 호출 반복!
}
// 문제 2: Undo/Redo 구현이 어려움
public class DrawingApp {
public void drawLine() {
// 선 그리기
// 어떻게 취소할까?
// 이전 상태를 어떻게 저장할까?
}
public void undo() {
// ??? 무엇을 되돌릴까?
}
}
// 문제 3: 작업 큐/로깅이 불가능
public class FileProcessor {
public void processFile(String file) {
// 파일 처리
// 나중에 실행하려면?
// 로그에 남기려면?
// 재실행하려면?
}
}
// 문제 4: 매크로/일괄 작업 어려움
public class ImageEditor {
public void adjustBrightness() { }
public void adjustContrast() { }
public void applyFilter() { }
// 이 작업들을 묶어서 "매크로"로 만들려면?
// 한 번에 실행하려면?
}- 강한 결합: 요청자와 수신자가 직접 연결
- Undo 불가: 작업 취소 어려움
- 작업 관리: 큐잉, 로깅, 스케줄링 불가
- 재사용 어려움: 같은 작업을 다양한 곳에서 사용 어려움
요청을 객체로 캡슐화하여 서로 다른 요청으로 클라이언트를 매개변수화하고, 요청을 큐에 저장하거나 로그로 기록하며, 취소 가능한 연산을 지원하는 패턴
- 요청 객체화: 요청을 객체로 만듦
- 디커플링: 요청자와 수신자 분리
- Undo/Redo: 작업 취소/재실행
- 작업 관리: 큐잉, 로깅, 스케줄링
// Before: 직접 호출
button.onClick(() -> {
editor.save(); // 강한 결합
});
// After: Command 객체
Command saveCommand = new SaveCommand(editor);
button.setCommand(saveCommand); // 약한 결합
button.click(); // command.execute()┌─────────────┐
│ Client │
└─────────────┘
│ creates
▼
┌─────────────┐ ┌──────────────┐
│ Invoker │ │ Command │ ← 명령 인터페이스
├─────────────┤ ├──────────────┤
│ - command │─────>│ + execute() │
│ + execute() │ │ + undo() │
└─────────────┘ └──────────────┘
△
│ implements
┌────────┴────────┐
│ │
┌────────────────┐ ┌───────────────┐
│ConcreteCommand │ │ConcreteCommand│
├────────────────┤ ├───────────────┤
│ - receiver │ │ - receiver │
│ + execute() │ │ + execute() │
│ + undo() │ │ + undo() │
└────────────────┘ └───────────────┘
│ │
│ calls │
▼ ▼
┌────────────────┐
│ Receiver │ ← 실제 작업 수행
├────────────────┤
│ + action() │
└────────────────┘
| 요소 | 역할 | 예시 |
|---|---|---|
| Command | 명령 인터페이스 | Command |
| ConcreteCommand | 구체적 명령 | SaveCommand |
| Receiver | 실제 작업 수행 | TextEditor |
| Invoker | 명령 실행 | Button |
| Client | 명령 생성 | Application |
/**
* Command: 명령 인터페이스
*/
public interface Command {
void execute();
void undo();
}
/**
* Receiver: 텍스트 에디터
*/
public class TextEditor {
private StringBuilder text;
public TextEditor() {
this.text = new StringBuilder();
}
public void write(String words) {
text.append(words);
System.out.println("✍️ 입력: " + words);
}
public void deleteLast(int length) {
if (text.length() >= length) {
text.delete(text.length() - length, text.length());
System.out.println("🗑️ 삭제: 마지막 " + length + "자");
}
}
public void copy() {
System.out.println("📋 복사: " + text.toString());
}
public void paste(String clipboard) {
text.append(clipboard);
System.out.println("📌 붙여넣기: " + clipboard);
}
public String getText() {
return text.toString();
}
public void display() {
System.out.println("📄 현재 텍스트: \"" + text.toString() + "\"");
}
}
/**
* ConcreteCommand 1: 쓰기 명령
*/
public class WriteCommand implements Command {
private TextEditor editor;
private String words;
public WriteCommand(TextEditor editor, String words) {
this.editor = editor;
this.words = words;
}
@Override
public void execute() {
editor.write(words);
}
@Override
public void undo() {
editor.deleteLast(words.length());
}
}
/**
* ConcreteCommand 2: 복사 명령
*/
public class CopyCommand implements Command {
private TextEditor editor;
public CopyCommand(TextEditor editor) {
this.editor = editor;
}
@Override
public void execute() {
editor.copy();
}
@Override
public void undo() {
System.out.println("↩️ 복사는 취소할 수 없습니다");
}
}
/**
* ConcreteCommand 3: 붙여넣기 명령
*/
public class PasteCommand implements Command {
private TextEditor editor;
private String clipboard;
public PasteCommand(TextEditor editor, String clipboard) {
this.editor = editor;
this.clipboard = clipboard;
}
@Override
public void execute() {
editor.paste(clipboard);
}
@Override
public void undo() {
editor.deleteLast(clipboard.length());
}
}
/**
* Invoker: 명령 실행자 (히스토리 관리)
*/
public class CommandHistory {
private Stack<Command> history;
private Stack<Command> redoStack;
public CommandHistory() {
this.history = new Stack<>();
this.redoStack = new Stack<>();
}
public void execute(Command command) {
command.execute();
history.push(command);
redoStack.clear(); // 새 명령 실행 시 redo 스택 초기화
}
public void undo() {
if (history.isEmpty()) {
System.out.println("⚠️ 되돌릴 작업이 없습니다");
return;
}
System.out.println("\n↩️ Undo 실행...");
Command command = history.pop();
command.undo();
redoStack.push(command);
}
public void redo() {
if (redoStack.isEmpty()) {
System.out.println("⚠️ 다시 실행할 작업이 없습니다");
return;
}
System.out.println("\n↪️ Redo 실행...");
Command command = redoStack.pop();
command.execute();
history.push(command);
}
public void showHistory() {
System.out.println("\n📚 명령 히스토리:");
System.out.println(" History: " + history.size() + " 개");
System.out.println(" Redo Stack: " + redoStack.size() + " 개");
}
}
/**
* 사용 예제
*/
public class CommandExample {
public static void main(String[] args) {
// Receiver 생성
TextEditor editor = new TextEditor();
// Invoker 생성
CommandHistory history = new CommandHistory();
// 작업 실행
System.out.println("=== 텍스트 편집 시작 ===\n");
history.execute(new WriteCommand(editor, "Hello"));
editor.display();
history.execute(new WriteCommand(editor, " World"));
editor.display();
history.execute(new CopyCommand(editor));
history.execute(new PasteCommand(editor, "!!!"));
editor.display();
// Undo
history.showHistory();
history.undo();
editor.display();
history.undo();
editor.display();
// Redo
history.redo();
editor.display();
history.showHistory();
}
}실행 결과:
=== 텍스트 편집 시작 ===
✍️ 입력: Hello
📄 현재 텍스트: "Hello"
✍️ 입력: World
📄 현재 텍스트: "Hello World"
📋 복사: Hello World
📌 붙여넣기: !!!
📄 현재 텍스트: "Hello World!!!"
📚 명령 히스토리:
History: 4 개
Redo Stack: 0 개
↩️ Undo 실행...
🗑️ 삭제: 마지막 3자
📄 현재 텍스트: "Hello World"
↩️ Undo 실행...
↩️ 복사는 취소할 수 없습니다
📄 현재 텍스트: "Hello World"
↪️ Redo 실행...
📋 복사: Hello World
📄 현재 텍스트: "Hello World"
📚 명령 히스토리:
History: 3 개
Redo Stack: 1 개
/**
* Receiver 1: TV
*/
public class TV {
private boolean on = false;
private int volume = 10;
private int channel = 1;
public void turnOn() {
on = true;
System.out.println("📺 TV 켜짐");
}
public void turnOff() {
on = false;
System.out.println("📺 TV 꺼짐");
}
public void volumeUp() {
volume++;
System.out.println("🔊 볼륨: " + volume);
}
public void volumeDown() {
volume--;
System.out.println("🔉 볼륨: " + volume);
}
public void setChannel(int channel) {
this.channel = channel;
System.out.println("📡 채널: " + channel);
}
public int getChannel() {
return channel;
}
public int getVolume() {
return volume;
}
}
/**
* Receiver 2: 조명
*/
public class Light {
private boolean on = false;
private int brightness = 50;
public void turnOn() {
on = true;
System.out.println("💡 조명 켜짐");
}
public void turnOff() {
on = false;
System.out.println("💡 조명 꺼짐");
}
public void dim() {
brightness = 30;
System.out.println("🔅 조명 어둡게: " + brightness + "%");
}
public void brighten() {
brightness = 100;
System.out.println("🔆 조명 밝게: " + brightness + "%");
}
public int getBrightness() {
return brightness;
}
}
/**
* ConcreteCommand: TV 켜기
*/
public class TVOnCommand implements Command {
private TV tv;
public TVOnCommand(TV tv) {
this.tv = tv;
}
@Override
public void execute() {
tv.turnOn();
}
@Override
public void undo() {
tv.turnOff();
}
}
/**
* ConcreteCommand: 조명 켜기
*/
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
@Override
public void undo() {
light.turnOff();
}
}
/**
* ConcreteCommand: 매크로 명령 (여러 명령 묶기)
*/
public class MacroCommand implements Command {
private Command[] commands;
public MacroCommand(Command[] commands) {
this.commands = commands;
}
@Override
public void execute() {
System.out.println("🎬 매크로 실행...");
for (Command command : commands) {
command.execute();
}
}
@Override
public void undo() {
System.out.println("↩️ 매크로 취소...");
// 역순으로 취소
for (int i = commands.length - 1; i >= 0; i--) {
commands[i].undo();
}
}
}
/**
* Invoker: 리모컨
*/
public class RemoteControl {
private Command[] onCommands;
private Command[] offCommands;
private Command lastCommand;
public RemoteControl() {
onCommands = new Command[7];
offCommands = new Command[7];
Command noCommand = new Command() {
public void execute() { }
public void undo() { }
};
for (int i = 0; i < 7; i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
lastCommand = noCommand;
}
public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
public void onButtonPressed(int slot) {
System.out.println("\n▶️ ON 버튼 " + slot + " 누름");
onCommands[slot].execute();
lastCommand = onCommands[slot];
}
public void offButtonPressed(int slot) {
System.out.println("\n⏹️ OFF 버튼 " + slot + " 누름");
offCommands[slot].execute();
lastCommand = offCommands[slot];
}
public void undoButtonPressed() {
System.out.println("\n↩️ UNDO 버튼 누름");
lastCommand.undo();
}
}
/**
* 사용 예제
*/
public class RemoteControlExample {
public static void main(String[] args) {
// Receiver 생성
TV tv = new TV();
Light light = new Light();
// Command 생성
Command tvOn = new TVOnCommand(tv);
Command tvOff = new Command() {
public void execute() { tv.turnOff(); }
public void undo() { tv.turnOn(); }
};
Command lightOn = new LightOnCommand(light);
Command lightOff = new Command() {
public void execute() { light.turnOff(); }
public void undo() { light.turnOn(); }
};
// Invoker 생성 및 설정
RemoteControl remote = new RemoteControl();
remote.setCommand(0, tvOn, tvOff);
remote.setCommand(1, lightOn, lightOff);
// 사용
System.out.println("=== 리모컨 사용 ===");
remote.onButtonPressed(0);
remote.onButtonPressed(1);
remote.undoButtonPressed();
// 매크로 (모든 기기 켜기)
System.out.println("\n=== 매크로 사용 ===");
Command[] partyOn = { tvOn, lightOn };
Command partyMacro = new MacroCommand(partyOn);
partyMacro.execute();
partyMacro.undo();
}
}/**
* Receiver: 파일 시스템
*/
public class FileSystem {
public void createFile(String name) {
System.out.println("📄 파일 생성: " + name);
}
public void deleteFile(String name) {
System.out.println("🗑️ 파일 삭제: " + name);
}
public void copyFile(String source, String dest) {
System.out.println("📋 파일 복사: " + source + " → " + dest);
}
}
/**
* ConcreteCommand: 파일 생성
*/
public class CreateFileCommand implements Command {
private FileSystem fs;
private String fileName;
public CreateFileCommand(FileSystem fs, String fileName) {
this.fs = fs;
this.fileName = fileName;
}
@Override
public void execute() {
fs.createFile(fileName);
}
@Override
public void undo() {
fs.deleteFile(fileName);
}
}
/**
* ConcreteCommand: 파일 삭제
*/
public class DeleteFileCommand implements Command {
private FileSystem fs;
private String fileName;
public DeleteFileCommand(FileSystem fs, String fileName) {
this.fs = fs;
this.fileName = fileName;
}
@Override
public void execute() {
fs.deleteFile(fileName);
}
@Override
public void undo() {
fs.createFile(fileName);
}
}
/**
* Invoker: 작업 큐
*/
public class TaskQueue {
private Queue<Command> queue;
public TaskQueue() {
this.queue = new LinkedList<>();
}
public void addTask(Command command) {
queue.offer(command);
System.out.println("➕ 작업 추가 (큐 크기: " + queue.size() + ")");
}
public void processAll() {
System.out.println("\n⚙️ 모든 작업 처리 시작...");
while (!queue.isEmpty()) {
Command command = queue.poll();
command.execute();
}
System.out.println("✅ 모든 작업 완료!");
}
public int size() {
return queue.size();
}
}
/**
* 사용 예제
*/
public class TaskQueueExample {
public static void main(String[] args) {
FileSystem fs = new FileSystem();
TaskQueue queue = new TaskQueue();
// 작업 추가
System.out.println("=== 작업 큐에 추가 ===");
queue.addTask(new CreateFileCommand(fs, "file1.txt"));
queue.addTask(new CreateFileCommand(fs, "file2.txt"));
queue.addTask(new DeleteFileCommand(fs, "old.txt"));
queue.addTask(new CreateFileCommand(fs, "file3.txt"));
// 일괄 처리
queue.processAll();
}
}| 장점 | 설명 | 예시 |
|---|---|---|
| 디커플링 | 요청자와 수신자 분리 | 리모컨과 TV |
| Undo/Redo | 작업 취소/재실행 | 텍스트 에디터 |
| 큐잉 | 작업 대기열 관리 | 작업 큐 |
| 로깅 | 명령 기록 가능 | 감사 로그 |
| 매크로 | 여러 명령 조합 | 파티 모드 |
| 단점 | 설명 | 해결책 |
|---|---|---|
| 클래스 증가 | 명령마다 클래스 | 익명 클래스/람다 |
| 복잡도 | 단순 작업에 과함 | 필요시에만 |
// 잘못된 예
public class BadCommand implements Command {
public void execute() {
// 복잡한 비즈니스 로직
// Command는 단순히 위임만!
}
}해결:
public class GoodCommand implements Command {
private Receiver receiver;
public void execute() {
receiver.action(); // 위임!
}
}✅ Command 인터페이스 정의
✅ ConcreteCommand 구현
✅ Receiver 정의
✅ Invoker 작성
✅ execute(), undo() 구현
| 상황 | 추천도 | 이유 |
|---|---|---|
| Undo/Redo 필요 | ⭐⭐⭐ | 히스토리 관리 |
| 작업 큐잉 | ⭐⭐⭐ | 지연 실행 |
| 매크로 | ⭐⭐⭐ | 명령 조합 |
| 로깅/감사 | ⭐⭐⭐ | 추적 가능 |
- 요청을 객체로
- 디커플링
- Undo 지원
- 작업 관리