본문 바로가기
IT/Java

[Java] Java Command Pattern

by kyu-nahc 2024. 9. 13.
반응형

 

Command Pattern

커맨드 패턴(Command Pattern)객체의 행위(메서드)를 클래스로 만들어 캡슐화하는 패턴이다.

어떤 객체(A)에서 다른 객체(B)의 메서드를 실행하려면,

객체(B)를 참조하고 있어야 하는 의존성이 발생한다.

이와 같은 상황에서 커맨드 패턴을 적용하면 의존성을 제거할 수 있다.

 

또한 기능이 수정되거나 변경이 일어날 때 A 클래스의 코드를 수정 없이

기능에 대한 클래스를 정의하면 되므로 시스템이 확장성이 있으면서 유연성을 가질 수 있다.

즉 커맨드 패턴은 연산을 호출하는 객체와 수행하는 객체를 분리하는 패턴이다.

이 패턴에서 핵심은 통상적으로 execute()라는

추상 메서드를 가진, Command 인터페이스를 통해 기능을 확장해 나간다.

Command 객체의 서브클래스들은 실제 수행에 관련된 객체를 인스턴수 변수로 저장하여

execute() 내부에서 실제 수행될 객체의 명령을 실행시킨다.

 

 

Command Pattern 구조

  • Command
    연산 수행에 필요한 인터페이스를 선언
  • ConcreateCommand A,B...
    Command 인터페이스를 구현한다. 객체와 액션 간의 연결성을 정의하고,
    처리 객체에 정의된 연산을 호출하도록 execute를 구현한다.
    단 여기서 Command는 Receiver 객체를 알고 있고,
    execute() 메서드에서 Recevier의 메서드를 호출한다.
  • Client
    Receiver / ConcreteCommand / Invoker 객체를 생성하고,
    필요한 요청에 따라 Invoker의 setCommand()를 호출하여 Command의 구현체를 바인딩한다.
    Invoker에게 원하는 Command의 수행 메서드를 요청한다.
  • Invoker
    Client의 요청을 받으며, 요청을 처리할 Command 구현체를 바인딩한다.
    Invoker는 Command Interface만 알고 있으며, Command의 구체적인 구현 내용은 알지 못한다.
  • Receiver
    요청에 관련된 연산 수행 방법을 알고 있고,
    ConcreteCommand 객체에 의해 Receiver의 action()이 호출된다.
    Client의 요청 로직의 실질적인 실행을 담당한다.

이에 대한 흐름도코드를 살펴보면 다음과 같다.

// 연산 수행에 필요한 인터페이스를 선언
public interface Command {
    void execute();
}
// Command 인터페이스를 구현
// execute() 메서드에서 Receiver의 메서드를 실행 
public class ConcreteCommandA implements Command{

    ReceiverA receiverA;

    // 생성자를 통해 ReceiverA 객체 할당
    public ConcreteCommandA(ReceiverA receiverA) {
        this.receiverA = receiverA;
    }
    
    @Override
    public void execute() {
        System.out.println("ConcreteCommandA executed");
        receiverA.action();
    }
}
// Command 인터페이스를 구현
// execute() 메서드에서 Receiver의 메서드를 실행 
public class ConcreteCommandB implements Command{
    ReceiverB receiverB;

    // 생성자를 통해 ReceiverB 객체 할당
    public ConcreteCommandB(ReceiverB receiverB) {
        this.receiverB = receiverB;
    }
    
    @Override
    public void execute() {
        System.out.println("ConcreteCommandB executed");
        receiverB.action();
    }
}
// ReceiverA 객체 생성
// Client의 요청 로직의 실질적인 실행을 담당
public class ReceiverA {
    public void action(){
        System.out.println("ReceiverA Actions");
    }
}
// ReceiveB 객체 생성
// Client의 요청 로직의 실질적인 실행을 담당
public class ReceiverB {

    public void action(){
        System.out.println("ReceiverB Actions");
    }
}
// Invoker 객체 생성
// Commmand setter가 존재
// executeAction()에서 Command의 execute() 메서드 실행
public class Invoker {

    Command command;

    public void setCommand(Command command) {
        System.out.println("Setting Command");
        this.command = command;
    }
    public void executeAction(){
        command.execute();
    }
}
public class Client {

    public static void main(String[] args) {
	
    	// 사용자의 요청을 받는 Invoker 객체 생성
        Invoker invoker = new Invoker();

        // ReceiverA 객체 생성
        ReceiverA receiverA = new ReceiverA();
        // ConcreteCommandA 객체를 생성
        // 생성자로 ReceiverA 객체 전달
        ConcreteCommandA concreteCommandA = new ConcreteCommandA(receiverA);
        // Invoker를 통해 Command 설정
        invoker.setCommand(concreteCommandA);
        // Client가 원하는 메인 로직 실행
        invoker.executeAction();

        // ReceiverB 객체 생성
        ReceiverB receiverB = new ReceiverB();
        // ConcreteCommandB 객체를 생성
        // 생성자로 ReceiverB 객체 전달
        ConcreteCommandB concreteCommandB = new ConcreteCommandB(receiverB);
      	// Invoker를 통해 Command 설정
        invoker.setCommand(concreteCommandB);
        // Client가 원하는 메인 로직 실행
        invoker.executeAction();
    }
}

 

 

 

Command Pattern 특징

패턴 적용 시기

Command 패턴은 요청을 캡슐화하여 서로 다른 요청을 객체화하고,

이 객체들을 파라미터로 전달하거나 저장할 수 있는 구조를 제공한다.

이 패턴을 사용할 때를 살펴보면 다음과 같다.

  • 요청을 파라미터화해야 할 때
    예를 들어, 사용자가 버튼을 클릭할 때 서로 다른 행동을 할 수 있도록
    요청을 캡슐화하고 전달해야 할 때 유용하다.
  • 요청을 큐에 저장하거나 로그에 기록할 때
    Command 객체는 요청을 캡슐화하므로 이를 큐에 저장하거나,

    나중에 재실행할 수 있도록 로그에 기록하는 것이 가능하다.
  • 요청을 되돌리거나 재실행해야 할 때
    Command 객체는 실행 메서드뿐 아니라 취소 메서드를 포함할 수 있어
    작업을 되돌리는 것이 가능하다. Undo, Redo 기능을 구현할 때 유용하다.
  • 메서드 호출의 발신자와 수신자 간의 결합을 느슨하게 하고 싶을 때
     발신자는 Command 객체를 통해 작업을 요청하고,
    실제 작업이 어떻게 수행되는지는 신경 쓰지 않게 된다.
    이렇게 하면 발신자와 수신자 사이의 결합도가 줄어든다.

패턴 장점

  • 요청의 캡슐화
    요청을 객체로 캡슐화함으로써 메서드 호출을 인수로 전달하거나,
    큐에 저장하거나, 나중에 재실행할 수 있다.
  • 유연한 확장성
    새로운 명령을 추가할 때 기존 코드를 수정하지 않고도
    새로운 Command 클래스를 생성하여 확장할 수 있다.
  • Undo / Redo 기능
    Command 객체에 실행 메서드와 취소 메서드를 구현하여
    작업을 취소하거나 재실행할 수 있다.
  • Invoker와 Receiver 간의 결합도 감소
    Invoker(요청을 실행하는 객체)와 Receiver(실제 작업을 수행하는 객체) 간의
    결합을 낮춰 서로 독립적으로 수정이 가능해진다.

패턴 단점

  • 클래스가 많아짐
    각각의 요청을 처리하기 위한 Command 클래스가 필요하므로 클래스 수가 늘어날 수 있다.
  • 복잡성 증가
    간단한 경우에도 모든 요청을 객체로 만들어야 하므로 코드가 복잡해질 수 있다.
    특히, 요청의 종류가 많아질수록 클래스의 수와 관리해야 할 코드가 늘어난다.

 

Command Pattern 적용 예시

실무에서 Command 패턴을 사용하는 더 적합한 예시는 복잡한 웹 애플리케이션이나
대규모 소프트웨어 시스템에서 주로 발생하는 작업 관리 시스템이나 트랜잭션 처리이다.
그중에서도 주문 관리 시스템(Order Management System)의 예시이다.

전자상거래 플랫폼에서 주문 처리, 결제, 환불, 배송 처리는 매우 중요하다.

이러한 시스템에서 Command 패턴을 사용하면,

각각의 작업(주문 생성, 결제 처리, 배송 처리, 주문 취소 등)을 객체로 캡슐화하여

유연하게 관리할 수 있다.  또한 작업을 로그에 저장해 나중에 복구하거나

트랜잭션 처리 시 작업을 되돌릴 수 있는 구조를 갖출 수 있다.

public interface OrderCommand {
    void execute();
    void undo(); // 작업 취소를 위한 메서드
}

public class CreateOrderCommand implements OrderCommand {
    private OrderReceiver processor;
    private String orderId;

    public CreateOrderCommand(OrderReceiver processor, String orderId) {
        this.processor = processor;
        this.orderId = orderId;
    }

    @Override
    public void execute() {
        processor.createOrder(orderId);
    }

    @Override
    public void undo() {
        processor.cancelOrder(orderId); // 생성된 주문 취소
    }
}

public class ProcessPaymentCommand implements OrderCommand {
    private OrderReceiver processor;
    private String orderId;

    public ProcessPaymentCommand(OrderReceiver processor, String orderId) {
        this.processor = processor;
        this.orderId = orderId;
    }

    @Override
    public void execute() {
        processor.processPayment(orderId);
    }

    @Override
    public void undo() {
        System.out.println("결제 취소: 주문 번호 " + orderId);
    }
}

public class ShipOrderCommand implements OrderCommand {
    private OrderReceiver processor;
    private String orderId;

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

    @Override
    public void execute() {
        processor.shipOrder(orderId);
    }

    @Override
    public void undo() {
        System.out.println("배송 취소: 주문 번호 " + orderId);
    }
}

 

Command Interface를 정의하고, 해당 인터페이스에는 2개의 메서드가 있다.

하나는 execute()이고, 하나는 undo()이다. execute()는 Receiver의 실행 메서드를 호출하고,

undo()는 Receiver의 실행 취소 메서드를 호출한다.

따라서 Receiver가 실질적인 요청을 실행하는 객체이고,

Command 에서는 Invoker의 요청에 맞게 Receiver의 메서드를 호출하는 역할만 한다.

public class OrderReceiver {
    public void createOrder(String orderId) {
        System.out.println("주문이 생성되었습니다. 주문 번호: " + orderId);
    }

    public void processPayment(String orderId) {
        System.out.println("결제가 처리되었습니다. 주문 번호: " + orderId);
    }

    public void shipOrder(String orderId) {
        System.out.println("주문이 배송되었습니다. 주문 번호: " + orderId);
    }

    public void cancelOrder(String orderId) {
        System.out.println("주문이 취소되었습니다. 주문 번호: " + orderId);
    }
}

public class OrderInvoker {
    private List<OrderCommand> commandHistory = new ArrayList<>();

    public void executeCommand(OrderCommand command) {
        command.execute();
        commandHistory.add(command); // 작업을 기록해 나중에 취소할 수 있도록 함
    }

    public void undoLastCommand() {
        if (!commandHistory.isEmpty()) {
            OrderCommand command = commandHistory.removeLast();
            command.undo();
        } else {
            System.out.println("취소할 작업이 없습니다.");
        }
    }
}

 

다음으로 Receiver와 Invoker 객체이다. Receiver는 Client 요청을 처리하는 로직이고,

해당 객체에서 최종적인 프로세스가 실행되는 것이다. Receiver는 메서드에 따라 로직을 실행하며,

이를 호출하는 것은 Command 인터페이스 구현체가 된다.

Invoker에서는 위의 구조 코드와 다르게 List로 Command 객체를 받도록 하여,

실행을 할 때마다 List에 추가해 준다. 이를 통해 undoLastCommand()를 실행할 경우,

마지막에 실행한 Command의 실행 로직을 취소할 수 있다.

이처럼 작업 기록을 남겨서 그전 상태로 되돌릴 수 있다는 장점이 있다.

public class Client {

    public static void main(String[] args) {
        OrderReceiver processor = new OrderReceiver();
        OrderInvoker orderManager = new OrderInvoker();

        String orderId = "ORD12345";

        // 명령 객체 생성
        OrderCommand createOrder = new CreateOrderCommand(processor, orderId);
        OrderCommand processPayment = new ProcessPaymentCommand(processor, orderId);
        OrderCommand shipOrder = new ShipOrderCommand(processor, orderId);

        // 주문 생성, 결제, 배송 처리
        orderManager.executeCommand(createOrder);
        orderManager.executeCommand(processPayment);
        orderManager.executeCommand(shipOrder);

        // 마지막 명령 취소 (배송 취소)
        orderManager.undoLastCommand();

        // 결제 취소
        orderManager.undoLastCommand();

        // 주문 취소
        orderManager.undoLastCommand();
    }
}

 

해당 코드에서의 Command 패턴의 역할을 살펴보면 다음과 같다.

 

요청 캡슐화

주문 생성, 결제 처리, 배송 등 여러 작업을 Command 객체로 캡슐화하여 관리 가능하다.

유연한 확장성

새로운 작업이 추가될 때 기존 코드를 수정하지 않고,

새로운 Command 클래스를 생성할 수 있다.

예를 들어, RefundCommand를 추가하여 환불 처리를 할 수 있다.

Undo 기능 제공

작업 기록을 남겨 나중에 작업을 취소(undo)할 수 있다.

특히 트랜잭션 기반의 작업을 되돌려야 할 때 매우 유용하게 사용 가능하다.

유지보수성 증가

Invoker(주문 관리자)와 Receiver(주문 처리자)는

서로 독립적으로 수정될 수 있으므로 유지보수가 쉬워진다.

 

이와 같이 Command 패턴은 복잡한 비즈니스 로직을 가진 시스템에서

작업을 관리하거나 취소해야 할 때 매우 유용하다.

트랜잭션 처리, 복잡한 비즈니스 프로세스 관리, 로깅 등을

간단하고 확장 가능한 구조로 만들 수 있으며, 요청을 캡슐화하여

클라이언트 측에서는 내부 로직에 접근하지 못하도록 구현할 수 있다.

따라서 트랜잭션 처리가 필요하거나 요청에 대해 정보 은닉을 필요할 경우

커맨드 패턴(Command Pattern)을 유용하게 사용할 수 있다.

 

 

 

참고자료

https://sup2is.github.io/2020/07/02/command-parttern.html

반응형

'IT > Java' 카테고리의 다른 글

[Java] Java Adapter Pattern  (1) 2024.09.15
[Java] Java Singleton Pattern  (2) 2024.09.14
[Java] Java Builder Pattern  (2) 2024.09.12
[Java] Java Factory Pattern  (1) 2024.09.11
[Java] Java Decorator Pattern  (0) 2024.09.09

loading