요청을 객체로 캡슐화하기
레스토랑을 생각해 보겠습니다. 고객이 "스테이크를 미디엄으로 주세요"라고 말하면, 웨이터는 이를 주문서에 적습니다. 웨이터는 주문서를 주방에 전달하고, 주방에서는 주문서에 따라 요리를 만듭니다. 필요에 따라 주문을 취소하거나 변경할 수도 있습니다.
여기서 주문서는 고객의 요청을 캡슐화한 커맨드입니다. 캡슐화라는 단어가 생소하게 들릴지 모르지만, 캡슐약을 생각하시면 됩니다. 캡슐 안에는 쓴 약가루가 들어있지만, 쓴맛을 느끼지 못하고 캡슐만 삼키면 됩니다. 또는 자동차를 생각해 보면 자동차 내부에는 복잡한 엔진, 변속기, 전자 장치들이 있지만, 운전은 핸들, 페달, 기어만으로 조작이 가능합니다.
캡슐화는 정보를 안전하게 포장하는 기술입니다. 기능을 사용함에 있어 복잡한 구조는 숨기고, 사용하는데 편리하게 감싼다는 개념입니다.
주문서가 캡슐화한 커맨드라는 말은 복잡하고 긴 고객의 요청을 주문서라는 간단하고 표준화된 형태로 포장하여, 누구나 쉽게 이해하고 처리할 수 있게 만든 것이라고 보시면 됩니다.
1980년대 후반 개발된 Smalltalk-80의 사용자 인터페이스 시스템에서는 이미 '액션(Action)' 또는 '커맨드(Command)' 객체를 사용하여 사용자 인터페이스 이벤트를 캡슐화하고 처리했습니다. 또한 1980년대 초반 객체지향 프로그래밍 언어인 Simula와 초기 Smalltalk에서도 메시지 전달 및 지연 실행 메커니즘으로 유사한 패턴이 사용되었습니다. 이를 1994년 GOF에 의해 커맨드 패턴이라고 공식화되었습니다.
레스토랑을 예제로 보겠습니다.
// Command 인터페이스 - 주문서의 기본 형태
interface Order {
void execute(); // 요리 실행
void cancel(); // 주문 취소
String getOrderDetails(); // 주문 상세 정보
}
// Receiver - 실제 요리를 하는 주방장
class Chef {
private String name;
public Chef(String name) {
this.name = name;
}
// 실제 요리 메서드들
public void cookSteak(String doneness) {
System.out.println(name + "이(가) 스테이크를 " + doneness + "로 굽습니다.");
}
public void cookPasta(String sauce) {
System.out.println(name + "이(가) " + sauce + " 파스타를 만듭니다.");
}
public void cookSalad(String dressing) {
System.out.println(name + "이(가) " + dressing + " 드레싱 샐러드를 준비합니다.");
}
public void stopCooking(String dish) {
System.out.println(name + ": " + dish + " 요리를 중단합니다.");
}
}
// Concrete Command - 구체적인 주문서들 : 스테이크 주문
class SteakOrder implements Order {
private Chef chef;
private String doneness;
private String tableNumber;
public SteakOrder(Chef chef, String doneness, String tableNumber) {
this.chef = chef;
this.doneness = doneness;
this.tableNumber = tableNumber;
}
@Override
public void execute() {
System.out.println("=== " + tableNumber + " 스테이크 주문 실행 ===");
chef.cookSteak(doneness);
}
@Override
public void cancel() {
System.out.println("=== " + tableNumber + " 스테이크 주문 취소 ===");
chef.stopCooking("스테이크");
}
@Override
public String getOrderDetails() {
return tableNumber + ": 스테이크 (" + doneness + ")";
}
}
// Concrete Command - 구체적인 주문서들 : 파스타 주문
class PastaOrder implements Order {
private Chef chef;
private String sauce;
private String tableNumber;
public PastaOrder(Chef chef, String sauce, String tableNumber) {
this.chef = chef;
this.sauce = sauce;
this.tableNumber = tableNumber;
}
@Override
public void execute() {
System.out.println("=== " + tableNumber + " 파스타 주문 실행 ===");
chef.cookPasta(sauce);
}
@Override
public void cancel() {
System.out.println("=== " + tableNumber + " 파스타 주문 취소 ===");
chef.stopCooking("파스타");
}
@Override
public String getOrderDetails() {
return tableNumber + ": 파스타 (" + sauce + ")";
}
}
// Invoker - 웨이터 (주문을 관리하고 실행)
class Waiter {
private String name;
private List<Order> orderList = new ArrayList<>();
private Stack<Order> completedOrders = new Stack<>();
public Waiter(String name) {
this.name = name;
}
// 주문 접수
public void takeOrder(Order order) {
orderList.add(order);
System.out.println(name + ": 주문을 접수했습니다 - " + order.getOrderDetails());
}
// 주문을 주방에 전달하고 실행
public void submitOrdersToKitchen() {
System.out.println("\n" + name + ": 주방에 주문을 전달합니다.");
for (Order order : orderList) {
order.execute();
completedOrders.push(order);
}
orderList.clear();
}
// 마지막 주문 취소 (실행 취소)
public void cancelLastOrder() {
if (!completedOrders.isEmpty()) {
Order lastOrder = completedOrders.pop();
lastOrder.cancel();
System.out.println(name + ": 마지막 주문을 취소했습니다.");
} else {
System.out.println(name + ": 취소할 주문이 없습니다.");
}
}
// 대기 중인 주문 목록 확인
public void showPendingOrders() {
System.out.println("\n=== " + name + "의 대기 주문 목록 ===");
if (orderList.isEmpty()) {
System.out.println("대기 중인 주문이 없습니다.");
} else {
for (int i = 0; i < orderList.size(); i++) {
System.out.println((i + 1) + ". " + orderList.get(i).getOrderDetails());
}
}
}
}
복합 주문이 있을 수 있으니 복합 주문까지 보겠습니다.
// 세트 메뉴 같은 복합 주문
class ComboOrder implements Order {
private List<Order> orders;
private String tableNumber;
// 여러개 주문을 받을 수 있다.
public ComboOrder(String tableNumber, Order... orders) {
this.tableNumber = tableNumber;
this.orders = Arrays.asList(orders);
}
@Override
public void execute() {
System.out.println("=== " + tableNumber + " 콤보 주문 실행 ===");
for (Order order : orders) {
order.execute();
}
}
@Override
public void cancel() {
System.out.println("=== " + tableNumber + " 콤보 주문 취소 ===");
// 역순으로 취소
for (int i = orders.size() - 1; i >= 0; i--) {
orders.get(i).cancel();
}
}
@Override
public String getOrderDetails() {
StringBuilder details = new StringBuilder(tableNumber + " 콤보: ");
for (Order order : orders) {
details.append(order.getOrderDetails()).append(" + ");
}
return details.substring(0, details.length() - 3); // 마지막 " + " 제거
}
}
위의 코드들을 실제 사용하면
public class RestaurantDemo {
public static void main(String[] args) {
// 레스토랑 직원들 생성
Chef chef = new Chef("김셰프");
Waiter waiter = new Waiter("박웨이터");
System.out.println("�️ 레스토랑이 오픈했습니다! �️\n");
// 고객들의 주문 생성 (Client가 Concrete Command 생성)
Order table1Steak = new SteakOrder(chef, "미디엄", "1번 테이블");
Order table2Pasta = new PastaOrder(chef, "크림", "2번 테이블");
Order table3Steak = new SteakOrder(chef, "웰던", "3번 테이블");
// 웨이터가 주문 접수
waiter.takeOrder(table1Steak);
waiter.takeOrder(table2Pasta);
waiter.takeOrder(table3Steak);
// 대기 주문 확인
waiter.showPendingOrders();
// 주문을 주방에 전달하고 실행
waiter.submitOrdersToKitchen();
System.out.println("\n--- 잠시 후 ---");
// 마지막 주문 취소 상황
System.out.println("고객: 죄송한데 3번 테이블 주문 취소해주세요!");
waiter.cancelLastOrder();
System.out.println("\n--- 새로운 주문 ---");
// 콤보 주문 (복합 커맨드)
Order comboSteak = new SteakOrder(chef, "미디엄레어", "4번 테이블");
Order comboPasta = new PastaOrder(chef, "토마토", "4번 테이블");
Order comboOrder = new ComboOrder("4번 테이블", comboSteak, comboPasta);
waiter.takeOrder(comboOrder);
waiter.showPendingOrders();
waiter.submitOrdersToKitchen();
System.out.println("\n고객: 아, 콤보 주문 취소할게요!");
waiter.cancelLastOrder();
}
}
실행결과는
�️ 레스토랑이 오픈했습니다! �️
박웨이터: 주문을 접수했습니다 - 1번 테이블: 스테이크 (미디엄)
박웨이터: 주문을 접수했습니다 - 2번 테이블: 파스타 (크림)
박웨이터: 주문을 접수했습니다 - 3번 테이블: 스테이크 (웰던)
=== 박웨이터의 대기 주문 목록 ===
1. 1번 테이블: 스테이크 (미디엄)
2. 2번 테이블: 파스타 (크림)
3. 3번 테이블: 스테이크 (웰던)
박웨이터: 주방에 주문을 전달합니다.
=== 1번 테이블 스테이크 주문 실행 ===
김셰프이(가) 스테이크를 미디엄로 굽습니다.
=== 2번 테이블 파스타 주문 실행 ===
김셰프이(가) 크림 파스타를 만듭니다.
=== 3번 테이블 스테이크 주문 실행 ===
김셰프이(가) 스테이크를 웰던로 굽습니다.
--- 잠시 후 ---
고객: 죄송한데 3번 테이블 주문 취소해주세요!
=== 3번 테이블 스테이크 주문 취소 ===
김셰프: 스테이크 요리를 중단합니다.
박웨이터: 마지막 주문을 취소했습니다.
--- 새로운 주문 ---
박웨이터: 주문을 접수했습니다 - 4번 테이블 콤보: 4번 테이블: 스테이크 (미디엄레어) + 4번 테이블: 파스타 (토마토)
=== 박웨이터의 대기 주문 목록 ===
1. 4번 테이블 콤보: 4번 테이블: 스테이크 (미디엄레어) + 4번 테이블: 파스타 (토마토)
박웨이터: 주방에 주문을 전달합니다.
=== 4번 테이블 콤보 주문 실행 ===
=== 4번 테이블 스테이크 주문 실행 ===
김셰프이(가) 스테이크를 미디엄레어로 굽습니다.
=== 4번 테이블 파스타 주문 실행 ===
김셰프이(가) 토마토 파스타를 만듭니다.
고객: 아, 콤보 주문 취소할게요!
=== 4번 테이블 콤보 주문 취소 ===
=== 4번 테이블 파스타 주문 취소 ===
김셰프: 파스타 요리를 중단합니다.
=== 4번 테이블 스테이크 주문 취소 ===
김셰프: 스테이크 요리를 중단합니다.
박웨이터: 마지막 주문을 취소했습니다.
고객의 요청이 주문서(Order 객체)에 작성되고, 웨이터는 고객의 요구를 실행(명령)만 하면 됩니다. 이후 요리는 셰프에 의해 처리가 됩니다. 고객이 셰프의 요리과정을 알 필요도 없고, 웨이터 또한 요리과정을 알 필요가 없습니다. 주문서라는 커맨드를 통해 실행하면 됩니다. 또한 여러 주문을 대기열에 넣고 순서대로 처리가 가능합니다.
커맨드 패턴은 "요청"을 "객체"로 만든다는 발상에서 시작하여, 유연하고 유지보수 가능한 시스템을 구축하는 데 큰 도움을 줍니다. 특히 요청과 실행의 분리가 중요한 상황에서 이 패턴이 적합합니다.
복잡한 시스템 속에서도 행동을 구체적인 명령 단위로 쪼개고, 실행 주체와 분리함으로써 효율적이고 반복 가능한 시스템을 만드는게 커맨드 패턴입니다. 잘 된 기업이나 조직은 커맨드 체계가 잘되어 있습니다. 군대라는 조직은 명령 체계로 이루어진 조직이고, 간단 명료하게 명령 단위가 나뉩니다.
자기계발 역시 막연한 목표 대신, “운동하기”, “책 읽기”, “감정 기록하기” 같은 구체적이고 실행 가능한 행동 목록으로 만들어 실천할 때 이루어집니다. 즉, 삶의 태스크를 명령화(Commanding Your Life) 하는 방식은 자신을 재설계하고 삶을 주체적으로 이끌어가는 핵심 도구가 될 수 있습니다.