Development Artist

Let's Dive Deep into the Command Pattern 본문

Research/Spring

Let's Dive Deep into the Command Pattern

JMcunst 2025. 3. 19. 18:28
728x90
반응형

서론

헤드 퍼스트 디자인 패턴 절반을 읽으면서 전체적인 패턴에 대해 실제 예시(결제 시스템)를 들어 정리를 했었다.

링크 : https://jmcunst.tistory.com/357

 

헤드 퍼스트 디자인 패턴 정리 -상- ( 결제 시스템 )

서론이번에 헤드 퍼스트 디자인 패턴이라는 책을 절반 정도 읽은 시점에서 한 번 정리를 해보고자 한다. 이 책은 꾀 유명한 책으로, 객체지향 설계를 고민하는 개발자에게 매우 도움이 되는 책

jmcunst.tistory.com

이번 글에서는 그중에서도 커맨드(Command) 패턴에 대해서 깊게 알아보려고 한다.


커맨드 패턴이란

커맨드를 직역하면 ‘명령’이다. 왜 커맨드 패턴으로 명명하였을까에 대해 고민해볼 필요가 있다.

 

우리는 코드에서 특정 동작을 실행할 때 메서드를 직접 호출하는 방식을 흔히 사용한다. 하지만 이 방식은 요청(명령)과 실행 대상(수행하는 객체)이 강하게 결합되기 때문에, 유지보수와 확장성이 떨어지는 문제가 있다.

 

Command 패턴은 이러한 문제를 해결하기 위해 ‘명령을 객체화’하는 패턴이다.

즉, 실행할 동작을 하나의 독립적인 객체(Command 객체) 로 감싸서 다룰 수 있도록 한다.

 

이 방식의 가장 큰 장점은 명령을 전달하고 실행하는 구조를 분리하여,

  • 실행할 명령을 저장하거나 큐에 넣어 나중에 실행할 수 있고,
  • 실행 취소(Undo) 기능을 쉽게 구현할 수 있으며,
  • 실행할 대상을 변경하더라도 기존 코드의 수정 없이 새로운 기능을 추가할 수 있다는 점이다.

Command 패턴을 적용하면 시스템의 유연성과 확장성이 증가하며, 명령을 클래스로 캡슐화함으로써 객체 지향적인 설계를 더욱 강화할 수 있다.


커맨드 패턴 없이 구현해보자

간단하게 리모컨(RemoteControl)으로 전등(Light)을 켜고 끄는 기능을 구현해보겠다.

class Light {
    public void turnOn() { System.out.println("Light is ON"); }
    public void turnOff() { System.out.println("Light is OFF"); }
}

class RemoteControl {
    private Light light;

    public RemoteControl(Light light) { this.light = light; }
    public void pressOnButton() { light.turnOn(); }
    public void pressOffButton() { light.turnOff(); }
}

// 실행
public class NoCommandPattern {
    public static void main(String[] args) {
        Light light = new Light();
        RemoteControl remote = new RemoteControl(light);

        remote.pressOnButton();  // 출력: Light is ON
        remote.pressOffButton(); // 출력: Light is OFF
    }
}

어떤가? RemoteControl Light 직접 의존하고 있다. 만약에 다른 기능(예: TV, 에어컨) 추가 시 RemoteControl의 코드도 수정해야 한다.

예를 들어, RemoteControl에 새로운 pressTVOnButton(), pressTVOffButton() 등의 메서드를 계속 추가해야 한다.

한번 전등 외에도 TV와 에어컨을 껐다가 키는 것을 추가해 보자.

// 기존 Light 클래스
class Light {
    public void turnOn() { System.out.println("Light is ON"); }
    public void turnOff() { System.out.println("Light is OFF"); }
}

// 새로운 TV 클래스 추가
class TV {
    public void turnOn() { System.out.println("TV is ON"); }
    public void turnOff() { System.out.println("TV is OFF"); }
}

// 새로운 에어컨 클래스 추가
class AirConditioner {
    public void turnOn() { System.out.println("Air Conditioner is ON"); }
    public void turnOff() { System.out.println("Air Conditioner is OFF"); }
}

// 아주 별로인 RemoteControl (기능이 추가될 때마다 코드가 커짐)
class RemoteControl {
    private Light light;
    private TV tv;
    private AirConditioner airConditioner;

    public RemoteControl(Light light, TV tv, AirConditioner airConditioner) {
        this.light = light;
        this.tv = tv;
        this.airConditioner = airConditioner;
    }

    public void pressLightOnButton() { light.turnOn(); }
    public void pressLightOffButton() { light.turnOff(); }
    public void pressTVOnButton() { tv.turnOn(); }
    public void pressTVOffButton() { tv.turnOff(); }
    public void pressAirConditionerOnButton() { airConditioner.turnOn(); }
    public void pressAirConditionerOffButton() { airConditioner.turnOff(); }
}

// 실행
public class BadRemoteControlExample {
    public static void main(String[] args) {
        Light light = new Light();
        TV tv = new TV();
        AirConditioner airConditioner = new AirConditioner();

        RemoteControl remote = new RemoteControl(light, tv, airConditioner);

        remote.pressLightOnButton();   // Light is ON
        remote.pressLightOffButton();  // Light is OFF

        remote.pressTVOnButton();      // TV is ON
        remote.pressTVOffButton();     // TV is OFF

        remote.pressAirConditionerOnButton();  // Air Conditioner is ON
        remote.pressAirConditionerOffButton(); // Air Conditioner is OFF
    }
}

RemoteControl 쪽이 굉장히 복잡해 진다. 그리고 명령을 제어하는 RemoteControl과 직접 실행하는 주체의 코드 전부를 만들어줘서 결합도가 높다. 만약 스마트 스피커, 커튼, 난방기 등을 추가해야 한다면? (holy...)

커맨드 패턴을 적용해서 우아하게 한번 바꿔보자.


커맨드 패턴 적용

커맨드라는 인터페이스를 만들고, 요청을 실행하는 클래스와 실제 동작을 수행하는 객체로 분리해서 구현한다.

// Command 인터페이스
interface Command {
    void execute();
}

// Receiver (실제 동작을 수행하는 객체)
class Light {
    public void turnOn() { System.out.println("Light is ON"); }
    public void turnOff() { System.out.println("Light is OFF"); }
}

// Command 구현 (Light 관련 명령)
class LightOnCommand implements Command {
    private Light light;

    public LightOnCommand(Light light) { this.light = light; }

    @Override
    public void execute() { light.turnOn(); }
}

class LightOffCommand implements Command {
    private Light light;

    public LightOffCommand(Light light) { this.light = light; }

    @Override
    public void execute() { light.turnOff(); }
}

// Invoker (요청을 실행하는 클래스)
class RemoteControl {
    private Command command;

    public void setCommand(Command command) { this.command = command; }
    public void pressButton() { command.execute(); }
}

// 실행 코드
public class CommandPatternExample {
    public static void main(String[] args) {
        Light light = new Light();
        Command lightOn = new LightOnCommand(light);
        Command lightOff = new LightOffCommand(light);

        RemoteControl remote = new RemoteControl();

        remote.setCommand(lightOn);
        remote.pressButton();  // 출력: Light is ON

        remote.setCommand(lightOff);
        remote.pressButton();  // 출력: Light is OFF
    }
}

TV와 에어컨을 추가해보면서 커맨드 패턴의 위력을 체감해보자.

interface Command {
    void execute();
}

class Light {
    public void turnOn() { System.out.println("Light is ON"); }
    public void turnOff() { System.out.println("Light is OFF"); }
}

class TV {
    public void turnOn() { System.out.println("TV is ON"); }
    public void turnOff() { System.out.println("TV is OFF"); }
}

class AirConditioner {
    public void turnOn() { System.out.println("Air Conditioner is ON"); }
    public void turnOff() { System.out.println("Air Conditioner is OFF"); }
}

// Light Commands
class LightOnCommand implements Command {
    private Light light;

    public LightOnCommand(Light light) { this.light = light; }

    @Override
    public void execute() { light.turnOn(); }
}

class LightOffCommand implements Command {
    private Light light;

    public LightOffCommand(Light light) { this.light = light; }

    @Override
    public void execute() { light.turnOff(); }
}

// TV Commands
class TVOnCommand implements Command {
    private TV tv;

    public TVOnCommand(TV tv) { this.tv = tv; }

    @Override
    public void execute() { tv.turnOn(); }
}

class TVOffCommand implements Command {
    private TV tv;

    public TVOffCommand(TV tv) { this.tv = tv; }

    @Override
    public void execute() { tv.turnOff(); }
}

// Air Conditioner Commands
class AirConditionerOnCommand implements Command {
    private AirConditioner airConditioner;

    public AirConditionerOnCommand(AirConditioner airConditioner) { this.airConditioner = airConditioner; }

    @Override
    public void execute() { airConditioner.turnOn(); }
}

class AirConditionerOffCommand implements Command {
    private AirConditioner airConditioner;

    public AirConditionerOffCommand(AirConditioner airConditioner) { this.airConditioner = airConditioner; }

    @Override
    public void execute() { airConditioner.turnOff(); }
}

class RemoteControl {
    private Command command;

    public void setCommand(Command command) { this.command = command; }
    public void pressButton() { command.execute(); }
}

public class CommandPatternExample {
    public static void main(String[] args) {
        // 리모컨 객체 생성
        RemoteControl remote = new RemoteControl();

        // Light 설정
        Light light = new Light();
        Command lightOn = new LightOnCommand(light);
        Command lightOff = new LightOffCommand(light);

        // TV 설정
        TV tv = new TV();
        Command tvOn = new TVOnCommand(tv);
        Command tvOff = new TVOffCommand(tv);

        // 에어컨 설정
        AirConditioner airConditioner = new AirConditioner();
        Command airConditionerOn = new AirConditionerOnCommand(airConditioner);
        Command airConditionerOff = new AirConditionerOffCommand(airConditioner);

        // 전등 조작
        remote.setCommand(lightOn);
        remote.pressButton();  // 출력: Light is ON

        remote.setCommand(lightOff);
        remote.pressButton();  // 출력: Light is OFF

        // TV 조작
        remote.setCommand(tvOn);
        remote.pressButton();  // 출력: TV is ON

        remote.setCommand(tvOff);
        remote.pressButton();  // 출력: TV is OFF

        // 에어컨 조작
        remote.setCommand(airConditionerOn);
        remote.pressButton();  // 출력: Air Conditioner is ON

        remote.setCommand(airConditionerOff);
        remote.pressButton();  // 출력: Air Conditioner is OFF
    }
}

요청을 하는 클래스에서 전혀 변경이 되는 것이 없었다! 결합이 느슨해진 것을 확인할 수 있다.


심화 3가지

실행 취소

커맨드를 인터페이스로 두었기 때문에 다양한 기능을 넣을 수 있다. 요청을 실행하는 클래스 실제 동작하는 클래스의 분리로 인해서 실제 동작 전에 요청을 취소할 수도 있게 된다!

아래의 코드를 보자.

// Command 인터페이스에 undo 기능 추가
interface Command {
    void execute();
    void undo();
}

// 기존 Light 클래스 사용

// Light On Command
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(); }
}

// Light Off Command
class LightOffCommand implements Command {
    private Light light;

    public LightOffCommand(Light light) { this.light = light; }

    @Override
    public void execute() { light.turnOff(); }

    @Override
    public void undo() { light.turnOn(); }
}

// Invoker (명령을 실행하고 Undo 지원)
class RemoteControl {
    private Stack<Command> history = new Stack<>();

    public void executeCommand(Command command) {
        command.execute();
        history.push(command);
    }

    public void undoCommand() {
        if (!history.isEmpty()) {
            Command command = history.pop();
            command.undo();
        }
    }
}

// 실행 코드
public class CommandPatternWithUndo {
    public static void main(String[] args) {
        Light light = new Light();
        RemoteControl remote = new RemoteControl();

        Command lightOn = new LightOnCommand(light);
        Command lightOff = new LightOffCommand(light);

        remote.executeCommand(lightOn);  // Light is ON
        remote.executeCommand(lightOff); // Light is OFF
        remote.undoCommand();            // Light is ON (Undo)
    }
}

코드를 살펴보면,

  • Command 인터페이스에 undo() 메서드를 추가하여 실행 취소 기능을 지원한다.
  • RemoteControl(Invoker)은 명령을 실행할 때 스택(Stack)에 명령을 저장하고, undoCommand()를 호출하면 마지막 명령을 되돌린다.
  • 이를 통해 Light(Receiver)에 대해 전원을 켜거나 끄는 동작을 실행 후 취소할 수 있다.

비동기 처리

Command 패턴을 사용하면 더욱 유연하고 확장성이 높은 비동기 처리가 가능하다.

우선 Command 패턴을 쓰지 않고 비동기 처리를 해보자.

class Light {
    public void turnOn() { System.out.println("Light is ON"); }
    public void turnOff() { System.out.println("Light is OFF"); }
}

class RemoteControl {
    private Light light;

    public RemoteControl(Light light) { this.light = light; }

    public void pressOnButton() { new Thread(() -> light.turnOn()).start(); } // 비동기 실행
    public void pressOffButton() { new Thread(() -> light.turnOff()).start(); } // 비동기 실행
    
}

// 실행 코드
public class NoCommandPatternAsync {
    public static void main(String[] args) {
        Light light = new Light();
        RemoteControl remote = new RemoteControl(light);

        remote.pressOnButton();  // 비동기 실행 -> Light is ON
        remote.pressOffButton(); // 비동기 실행 -> Light is OFF
    }
}

위의 코드new Thread(() -> light.turnOn()).start();처럼 실행한 뒤에는 추적이 어렵다. 

 

또한, Command 패턴을 채택하지 않기 때문에 앞서 확인했던 것 처럼, 새로운 기능을 추가하려면 RemoteControl 클래스의 코드를 계속 수정해야 한다. 이는 코드의 유지보수를 어렵게 만들고 확장성을 떨어뜨린다.

 

더욱이, 실행된 작업을 저장하고 나중에 실행하거나 재실행하는 기능(재시도, Undo 등)을 구현하기 어렵다.

즉, RemoteControl 클래스가 실행의 흐름을 직접 제어하고 있기 때문에, 실행된 명령을 기록하거나 다시 실행하는 기능을 추가하려면 복잡한 상태 관리가 필요하며, 코드가 점점 더 복잡해질 것이다.

 

이제 Command 패턴을 적용하여 이러한 문제를 해결해보자.

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

// Command 인터페이스
interface Command {
    void execute();
}

// Receiver (실제 작업을 수행하는 객체)
class Light {
    public void turnOn() { System.out.println("Light is ON"); }
    public void turnOff() { System.out.println("Light is OFF"); }
}

// Concrete Command (비동기 작업을 위한 명령)
class LightOnCommand implements Command {
    private Light light;

    public LightOnCommand(Light light) { this.light = light; }

    @Override
    public void execute() { light.turnOn(); }
}

class LightOffCommand implements Command {
    private Light light;

    public LightOffCommand(Light light) { this.light = light; }

    @Override
    public void execute() {
        light.turnOff();
    }
}

// Command Processor (Worker Thread)
class CommandProcessor extends Thread {
    private BlockingQueue<Command> queue = new LinkedBlockingQueue<>();

    public void addCommand(Command command) {
        queue.add(command);
    }

    @Override
    public void run() {
        while (true) {
            try {
                Command command = queue.take(); // 큐에서 명령을 가져옴 (Blocking)
                command.execute(); // 명령 실행
            } catch (InterruptedException e) { break; }
        }
    }
}

// 실행 코드
public class AsyncCommandPattern {
    public static void main(String[] args) {
        Light light = new Light();
        CommandProcessor processor = new CommandProcessor();
        processor.start(); // Worker Thread 시작

        // 비동기 명령 추가
        processor.addCommand(new LightOnCommand(light));
        processor.addCommand(new LightOffCommand(light));

        // 2초 대기 후 스레드 종료
        try { Thread.sleep(2000); } catch (InterruptedException e) {}
        processor.interrupt();
    }
}

위의 코드를 보면 queue.add(command);를 통해 명령을 저장하고 다시 실행이 가능하다. 명령을 객체로 저장할 수 있기 때문에, 실행된 작업을 추적하고 나중에 다시 실행할 수도 있다.

 

또한, 명령을 큐에 저장한 후 비동기적으로 실행할 수 있기 때문에, 작업을 순차적으로 실행하거나 특정 시점에서 실행을 지연할 수도 있다.

 

예를 들어, 네트워크가 불안정할 때 즉시 실행하지 않고, 안정적인 시점에서 실행할 수 있도록 예약하는 기능을 쉽게 추가할 수 있다.

이는 단순한 new Thread(() -> light.turnOn()).start(); 방식과 비교했을 때 큰 장점이 된다.

 

그리고 BlockingQueue를 사용하여 작업을 동기화하기 때문에, 여러 개의 스레드에서 안전하게 실행 가능하다. Java의 BlockingQueue스레드 간 안전한 작업 큐(Synchronized Queue)를 제공하여, 동시에 여러 스레드가 접근해도 충돌 없이 안정적으로 실행할 수 있도록 해준다.

 

그리고 현재 예시는 단일 Worker Thread이지만, 여러 개의 CommandProcessor를 실행하면 병렬 처리도 가능하다. 이를 통해 여러 개의 이메일 전송을 동시에 처리하는 시스템을 구현할 때 큰 이점이 된다.

아래의 이메일 전송 시스템 예시 코드를 한번 보자.

import java.util.concurrent.*;

// Command 인터페이스
interface Command {
    void execute();
}

// Receiver (실제 작업을 수행하는 객체)
class EmailService {
    public void sendEmail(String message) {
        System.out.println(Thread.currentThread().getName() + " -> Sending Email: " + message);
        try {
            Thread.sleep(1000); // 이메일 전송 시뮬레이션
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println(Thread.currentThread().getName() + " -> Email Sent: " + message);
    }
}

// Concrete Command (비동기 작업을 위한 명령)
class SendEmailCommand implements Command {
    private EmailService emailService;
    private String message;

    public SendEmailCommand(EmailService emailService, String message) {
        this.emailService = emailService;
        this.message = message;
    }

    @Override
    public void execute() {
        emailService.sendEmail(message);
    }
}

// Worker Thread Pool (여러 개의 Worker가 동시에 실행)
class CommandProcessor {
    private BlockingQueue<Command> queue = new LinkedBlockingQueue<>();
    private ExecutorService executorService;

    public CommandProcessor(int threadCount) {
        executorService = Executors.newFixedThreadPool(threadCount); // 멀티스레드 환경
        for (int i = 0; i < threadCount; i++) {
            executorService.execute(() -> {
                while (true) {
                    try {
                        Command command = queue.take(); // 큐에서 명령 가져오기 (Blocking)
                        command.execute(); // 명령 실행
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            });
        }
    }

    public void addCommand(Command command) {
        queue.add(command); // 명령을 큐에 추가
    }

    public void shutdown() {
        executorService.shutdownNow(); // 모든 스레드 종료
    }
}

// 실행 코드
public class MultiThreadedAsyncCommand {
    public static void main(String[] args) {
        EmailService emailService = new EmailService();
        CommandProcessor processor = new CommandProcessor(3); // 3개의 Worker Thread 실행

        // 여러 개의 비동기 이메일 전송 요청 추가
        processor.addCommand(new SendEmailCommand(emailService, "Welcome to our service!"));
        processor.addCommand(new SendEmailCommand(emailService, "Your order has been shipped."));
        processor.addCommand(new SendEmailCommand(emailService, "Don't forget to use your discount code."));
        processor.addCommand(new SendEmailCommand(emailService, "New updates are available."));
        processor.addCommand(new SendEmailCommand(emailService, "Reminder: Your subscription is expiring soon."));

        // 5초 후 스레드 종료
        try { Thread.sleep(5000); } catch (InterruptedException e) {}
        processor.shutdown();
    }
}

로컬에서 실행을 시켜보면 아래 처럼 출력된다.

pool-1-thread-1 -> Sending Email: Welcome to our service!
pool-1-thread-2 -> Sending Email: Your order has been shipped.
pool-1-thread-3 -> Sending Email: Don't forget to use your discount code.
pool-1-thread-1 -> Email Sent: Welcome to our service!
pool-1-thread-1 -> Sending Email: New updates are available.
pool-1-thread-2 -> Email Sent: Your order has been shipped.
pool-1-thread-2 -> Sending Email: Reminder: Your subscription is expiring soon.
pool-1-thread-3 -> Email Sent: Don't forget to use your discount code.
pool-1-thread-1 -> Email Sent: New updates are available.
pool-1-thread-2 -> Email Sent: Reminder: Your subscription is expiring soon.

여러 개의 Worker Thread가 동시에 이메일을 전송하며, 병렬로 실행되는 것을 확인할 수 있다.


EDA 기반 대규모 시스템

Command 패턴을 비동기 메시지 큐 (RabbitMQ, Kafka)와 결합하여 확장성 높은 분산 시스템을 구축할 수 있다. 이를 통해, 서비스 간 결합도를 낮추고 비동기 이벤트 기반 아키텍처를 구축할 수 있다.

 

앞서 얘기한 Command 패턴의 장점이 이벤트 드리븐 시스템에서 마찬가지로 적용된다.

  • 비동기 처리: Command 패턴을 사용하여 즉시 실행되지 않고 비동기적으로 실행 가능
  • 확장 가능성: 새로운 서비스 추가 시 기존 시스템을 수정할 필요 없음
  • 서비스 간 결합도 감소: 예를 들어, A 서비스와 B 서비스가 직접 통신하는 대신, 메시지 큐(RabbitMQ, Kafka)를 통해 느슨한 결합 가져갈 수 있음(Loose Coupling)
[Microservice A] → (Command 객체 생성) → [메시지 큐(RabbitMQ/Kafka)] → (Worker가 Command 실행) → [Microservice B]

 

주문과 배송 시스템을 예시로 구현해보자.

우선 주문을 담당하는 서버에서는 주문이 발생하면 Kafka의 order_created 토픽에 메시지를 전송하도록 한다.

import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

@Service
public class OrderEventProducer {
    private final KafkaTemplate<String, String> kafkaTemplate;

    public OrderEventProducer(KafkaTemplate<String, String> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public void sendOrderCreatedEvent(String orderId) {
        kafkaTemplate.send("order_created", orderId);
        System.out.println("주문 생성 이벤트 전송: " + orderId);
    }
}
public class PlaceOrderCommand implements Command {
    private final String orderId;
    private final OrderEventProducer orderEventProducer;

    public PlaceOrderCommand(String orderId, OrderEventProducer orderEventProducer) {
        this.orderId = orderId;
        this.orderEventProducer = orderEventProducer;
    }

    @Override
    public void execute() {
        System.out.println("주문 생성: " + orderId);
        orderEventProducer.sendOrderCreatedEvent(orderId);
    }
}
import org.springframework.stereotype.Service;

@Service
public class OrderService {
    private final OrderEventProducer orderEventProducer;

    public OrderService(OrderEventProducer orderEventProducer) {
        this.orderEventProducer = orderEventProducer;
    }

    public void placeOrder(String orderId) {
        Command command = new PlaceOrderCommand(orderId, orderEventProducer);
        command.execute();
    }
}
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/orders")
public class OrderController {
    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping("/{orderId}")
    public String placeOrder(@PathVariable String orderId) {
        orderService.placeOrder(orderId);
        return "주문 완료: " + orderId;
    }
}

정리를 해보자면,

  • /orders/{orderId} API를 호출하면 orderService 호출
  • orderService 는 PlaceOrderCommand 를 생성 후 실행
  • PlaceOrderCommand는 Kafka를 통해 주문 생성 이벤트를 발행

이제 배송을 담당하는 서버에서는 Kafka의 order_created 이벤트를 수신하여 배송을 자동으로 처리하게 된다.

import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;

@Service
public class ShippingEventConsumer {

    @KafkaListener(topics = "order_created", groupId = "shipping_group")
    public void processOrder(String orderId) {
        System.out.println("배송 요청 접수: " + orderId);
        Command command = new ShipOrderCommand(orderId);
        command.execute();
    }
}
public class ShipOrderCommand implements Command {
    private final String orderId;

    public ShipOrderCommand(String orderId) {
        this.orderId = orderId;
    }

    @Override
    public void execute() {
        System.out.println("배송 시작: " + orderId);
    }
}

정리해보자면, 

  • ShippingService가 @KafkaListener를 통해 메시지를 수신
  • ShipOrderCommand가 실행되어 배송이 자동으로 시작됨

결론

이번 글에서는 Command 패턴에 대해 설명하고 이를 활용하여 Undo, 비동기 처리 및 이벤트 기반 아키텍처(EDA, Event-Driven Architecture)를 구축하는 과정을 살펴보았다.

 

Command 패턴을 단순한 리모컨 예제에서 출발하여, 비동기 처리, Undo 기능, 멀티스레드 환경, 그리고 대규모 이벤트 기반 시스템으로 확장해 나가면서 Command 패턴이 갖는 강력한 확장성과 유지보수성의 가치를 확인할 수 있었다.

 

마지막으로 정리해보자면,

기능 Command 패턴 적용 시 장점
결합도 감소 요청을 수행하는 객체(Invoker)와 실제 동작(Receiver)이 분리됨
기능 확장 용이 새로운 기능을 추가할 때 기존 코드 수정 없이 새로운 Command 객체만 추가
비동기 처리 가능 BlockingQueue 또는 메시지 큐(Kafka, RabbitMQ)와 결합하여 비동기 실행 가능
Undo/Redo 명령을 객체로 저장하므로 실행 취소(Undo) 가능
멀티스레드 환경에서 안전성 증가 BlockingQueue ExecutorService를 활용하여 안전한 병렬 실행 가능
대규모 시스템 확장 가능 Kafka 등 메시지 큐를 활용하여 분산 시스템을 효율적으로 구축 가능

 

Command 패턴은 단순한 디자인 패턴을 넘어, 다양한 아키텍처에서 활용될 수 있을 것으로 기대한다.

728x90
반응형
Comments