JAVA

커맨드 패턴(Command Pattern)이란?

Stark97 2024. 10. 4. 21:24
반응형
 
 
 

Command 패턴 알아보기

 

1. Command 패턴이 왜 필요하고 어떤 장점을 제공할까?

지금부터 레스토랑 주문 시스템을 예로 들어, Command 패턴이 왜 필요하고 어떤 장점을 제공하는지 알아보자. 

 

Command 패턴을 사용하지 않은 경우

  • 먼저, Command 패턴을 사용하지 않고 간단한 주문 시스템을 구현해 보자.
// OrderSystem 클래스: 주문 처리 담당
public class OrderSystem {

    public void placeOrder(String item) {
        System.out.println(item + " 주문이 접수되었습니다.");
    }

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

// RestaurantApp 클래스: 메인 애플리케이션
public class RestaurantApp {

    public static void main(String[] args) {
        OrderSystem orderSystem = new OrderSystem();

        // 피자 주문
        orderSystem.placeOrder("피자");

        // 파스타 주문
        orderSystem.placeOrder("파스타");

        // 피자 주문 취소
        orderSystem.cancelOrder("피자");
    }
    
}

실행 결과

피자 주문이 접수되었습니다.
파스타 주문이 접수되었습니다.
피자 주문이 취소되었습니다.

문제점

  1. 높은 결합도: RestaurantApp은 OrderSystem의 메서드를 직접 호출하여 두 클래스 간 결합도가 높다.
  2. 확장성의 한계: 새로운 기능(예: 주문 수정)을 추가하려면 OrderSystemRestaurantApp 클래스에 메서드를 추가하거나 수정해야 한다.
  3. 유연성 부족: 주문 처리를 지연시키거나, 주문 내역을 저장하는 등의 기능을 추가하기 어렵다.
  4. 유지보수 어려움: 코드가 복잡해질수록 수정과 관리가 어려워진다.

2. Command 패턴 소개

Command 패턴이란?

  • Command 패턴은 요청을 객체로 캡슐화하여, 서로 다른 요청에 대해 사용자가 매개변수화를 할 수 있게 해주는 디자인 패턴이다. 이를 통해 요청의 발신자(Invoker)와 수신자(Receiver)를 분리하여 시스템의 유연성과 확장성을 높일 수 있다.

  • Command 패턴을 사용하면 다음과 같은 이점을 얻을 수 있다.
    1. 결합도 감소: 요청의 발신자와 수신자 사이의 결합도를 낮출 수 있다.
    2. 유연한 요청 처리: 요청을 객체로 표현하여 다양한 방식으로 처리할 수 있다.
    3. 명령의 캡슐화: 실행에 필요한 모든 정보를 명령 객체에 캡슐화하여 관리할 수 있다.

Command 패턴의 구성 요소 (클래스)

  1. Command 인터페이스
    • 모든 커맨드가 구현해야 하는 인터페이스
    • 주로 execute() 메서드를 포함

  2. ConcreteCommand
    • Command 인터페이스를 구현하는 실제 클래스
    • 특정 Receiver와 작업을 연결

  3. Receiver
    • 실제 작업을 수행하는 클래스
    • ConcreteCommand에 의해 호출됨

  4. Invoker
    • Command 객체를 저장하고 실행하는 클래스
    • 언제 명령을 실행할지 결정

  5. Client (main 메서드)
    • ConcreteCommand 객체를 생성하고 Receiver를 설정

3. Command 패턴의 작동 방식

  1. Client에서 ConcreteCommand 객체를 생성하고, 필요한 데이터를 설정한 후 Receiver 객체를 지정한다.
  2. 생성된 Command 객체를 Invoker 객체에게 전달한다.
  3. Invoker 객체는 필요한 시점에 Command 객체의 execute() 메서드를 호출한다.
  4. ConcreteCommand 객체의 execute() 메서드는 Receiver의 메서드를 호출하여 실제 작업을 수행한다.

 

3. Command 패턴을 적용한 코드

이제 Command 패턴을 적용하여 주문 시스템을 구현해 보자.

 

1. Command 인터페이스

// Command 인터페이스: 모든 명령의 공통 메서드를 정의
public interface Command {
    void execute();  // 명령을 실행하는 메서드
}

2. Receiver 클래스

// OrderSystem 클래스: 실제 주문 처리를 담당 (Receiver)
public class OrderSystem {

    public void placeOrder(String item) {
        System.out.println(item + " 주문이 접수되었습니다.");
    }

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

3. Concrete Command 클래스들

  • 주문 명령(command) 클래스
// OrderCommand 클래스: 주문 명령을 나타냄
public class OrderCommand implements Command {

    private OrderSystem orderSystem;
    private String item;

    public OrderCommand(OrderSystem orderSystem, String item) {
        this.orderSystem = orderSystem;
        this.item = item;
    }

    @Override
    public void execute() {
        orderSystem.placeOrder(item);
    }
    
}
  • 주문 취소 명령(command) 클래스
// CancelOrderCommand 클래스: 주문 취소 명령을 나타냄
public class CancelOrderCommand implements Command {

    private OrderSystem orderSystem;
    private String item;

    public CancelOrderCommand(OrderSystem orderSystem, String item) {
        this.orderSystem = orderSystem;
        this.item = item;
    }

    @Override
    public void execute() {
        orderSystem.cancelOrder(item);
    }
    
}

4. Invoker(Waiter) 클래스

// Waiter 클래스: 명령을 받아 실행하는 역할 (Invoker)
public class Waiter {

    private Command command;

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

    public void executeCommand() {
        command.execute();
    }
    
}

5. Client 클래스

// RestaurantApp 클래스: 전체 시스템을 조합하고 실행 (Client)
public class RestaurantApp {

    public static void main(String[] args) {
        // 주문 시스템 생성 (Receiver)
        OrderSystem orderSystem = new OrderSystem();

        // 웨이터 생성 (Invoker)
        Waiter waiter = new Waiter();

        // 피자 주문 명령 생성 (Concrete Command)
        Command pizzaOrder = new OrderCommand(orderSystem, "피자");
        // 웨이터에게 명령 전달
        waiter.setCommand(pizzaOrder);
        // 웨이터가 명령 실행
        waiter.executeCommand();

        // 파스타 주문 명령 생성
        Command pastaOrder = new OrderCommand(orderSystem, "파스타");
        waiter.setCommand(pastaOrder);
        waiter.executeCommand();

        // 피자 주문 취소 명령 생성
        Command cancelPizzaOrder = new CancelOrderCommand(orderSystem, "피자");
        waiter.setCommand(cancelPizzaOrder);
        waiter.executeCommand();
    }
    
}

실행 결과

피자 주문이 접수되었습니다.
파스타 주문이 접수되었습니다.
피자 주문이 취소되었습니다.

 

4. Command 패턴의 장점

Command 패턴을 적용함으로써 다음과 같은 장점을 얻을 수 있다.

 

1. 요청 발신자와 수신자의 분리

  • Waiter(Invoker)는 Command 인터페이스만 알고 있으며, 명령의 구체적인 실행 방법은 알 필요가 없다.
  • 명령의 실행은 Command 객체에 위임되므로, Waiter는 어떤 명령이 실행되는지에 대해 독립적이다.

코드 예시와 장점

  • 유연성 증가: Waiter는 다양한 명령을 실행할 수 있으며, 새로운 명령이 추가되더라도 Waiter의 코드를 수정할 필요가 없다.
  • 변경에 강한 구조: 명령의 실행 방법이 변경되더라도 Waiter(Invoker) 객체는 영향을 받지 않는다.
// Waiter 클래스 내부
public void executeCommand() {
    command.execute(); // 명령의 구체적인 내용은 모름
}

2. 캡슐화

  • 명령을 객체로 캡슐화하여 실행에 필요한 모든 정보를 하나의 객체(Command 구현 클래스)로 관리한다.
  • 이는 코드의 응집도를 높이고, 명령 관련 로직을 한 곳에 모아 관리할 수 있게 해 준다.

코드 예시와 장점

  • 높은 응집도: 명령 실행에 필요한 데이터와 로직이 하나의 객체에 모여 있다.
  • 관리 용이성: 명령별로 클래스를 분리하여 관리하므로, 코드의 가독성과 유지보수성이 향상된다.
public class OrderCommand implements Command {

    private OrderSystem orderSystem;
    private String item;

    // 명령 실행에 필요한 정보(item)가 객체 내부에 캡슐화됨
    public OrderCommand(OrderSystem orderSystem, String item) {
        this.orderSystem = orderSystem;
        this.item = item;
    }

    @Override
    public void execute() {
        orderSystem.placeOrder(item);
    }
    
}

3. 확장성

  • 새로운 기능을 추가하려면 새로운 Command 클래스를 생성하기만 하면 된다.
  • 기존 코드에 영향을 주지 않고 기능을 확장할 수 있다.

코드 예시와 장점

  • 기능 추가의 용이성: 새로운 명령을 추가하기 위해 기존 클래스들을 수정할 필요가 없다.
  • 개방-폐쇄 원칙 준수(OCP): 기존 코드의 수정 없이 기능을 확장할 수 있다. (command 객체를 만들고 사용하면 된다.)
// 새로운 기능: 주문 수정 명령 추가
public class ModifyOrderCommand implements Command {

    private OrderSystem orderSystem;
    private String oldItem;
    private String newItem;

    public ModifyOrderCommand(OrderSystem orderSystem, String oldItem, String newItem) {
        this.orderSystem = orderSystem;
        this.oldItem = oldItem;
        this.newItem = newItem;
    }

    @Override
    public void execute() {
        orderSystem.cancelOrder(oldItem);
        orderSystem.placeOrder(newItem);
    }
    
}

// 사용 예시
Command modifyOrder = new ModifyOrderCommand(orderSystem, "피자", "스파게티");
waiter.setCommand(modifyOrder);
waiter.executeCommand();

4. 유연성

  • 명령 객체를 저장하거나, 실행을 지연시키거나, 취소할 수 있다.
  • 예를 들어, 명령을 큐에 저장하여 순차적으로 실행하거나, 매크로 명령을 만들 수 있다.

코드 예시와 장점

  • 작업의 스케줄링: 명령 실행 시점을 제어할 수 있다.
  • 매크로 명령 구현: 여러 명령을 조합하여 하나의 작업으로 실행할 수 있다.
  • 취소 및 재실행: 명령 내역을 저장하여 작업을 취소하거나 재실행할 수 있다.
// 명령을 저장하는 큐
Queue<Command> commandQueue = new LinkedList<>();

// 명령 추가
commandQueue.add(pizzaOrder);
commandQueue.add(pastaOrder);
commandQueue.add(cancelPizzaOrder);

// 명령 실행
while (!commandQueue.isEmpty()) {
    Command cmd = commandQueue.poll();
    waiter.setCommand(cmd);
    waiter.executeCommand();
}

5. 재사용성

  • 동일한 Receiver(OrderSystem)를 사용하여 다양한 명령 객체를 만들 수 있다.
  • 명령 클래스들은 서로 독립적이므로 재사용성이 높다.

코드 예시와 장점

  • 코드 중복 감소: 공통 로직은 Receiver에 구현하고, 명령 클래스들은 필요한 부분만 구현한다.
  • 유지보수성 향상: 재사용 가능한 코드가 많아지므로 수정 시 영향 범위가 줄어든다.
// 다양한 명령 객체 생성
Command pizzaOrder = new OrderCommand(orderSystem, "피자");
Command pastaOrder = new OrderCommand(orderSystem, "파스타");
Command cancelPizzaOrder = new CancelOrderCommand(orderSystem, "피자");


// OrderSystem 클래스: 실제 주문 처리를 담당 (Receiver)
public class OrderSystem {

    public void placeOrder(String item) {
        System.out.println(item + " 주문이 접수되었습니다.");
    }

    public void cancelOrder(String item) {
        System.out.println(item + " 주문이 취소되었습니다.");
    }
    
    // 공통 로직 추가
    
}

// 주문 명령 클래스
public class OrderCommand implements Command {

    private OrderSystem orderSystem;
    private String item;

    // 명령 실행에 필요한 정보(item)가 객체 내부에 캡슐화됨
    public OrderCommand(OrderSystem orderSystem, String item) {
        this.orderSystem = orderSystem;
        this.item = item;
    }

    @Override
    public void execute() {
        orderSystem.placeOrder(item);
    }
    
}

6. 유지보수성 향상

  • 코드의 각 부분이 명확하게 분리되어 있다.
  • 수정이나 확장이 필요한 부분만 손쉽게 찾아 수정할 수 있다.

 

장점

  • 높은 응집도와 낮은 결합도: 클래스 간 의존성이 줄어들어 변경의 영향이 최소화된다.
  • 코드 가독성 향상: 역할별로 코드가 분리되어 있어 이해하기 쉽다.
  • 테스트 용이성: 각 명령 객체를 독립적으로 테스트할 수 있다.

 

5. Command 패턴 비교 다이어그램

패턴 미적용 코드 구조

  • RestaurantApp이 OrderSystem의 메서드를 직접 호출하여 두 클래스 간 강한 결합도를 갖는다.
+-----------------+           +-----------------+
|   RestaurantApp |           |   OrderSystem   |
|-----------------|  calls    |-----------------|
| - main()        +---------->+ - placeOrder()  |
|                 |           | - cancelOrder() |
+-----------------+           +-----------------+

Command 패턴 적용 코드 구조

+-----------------+        +---------+        +-----------------+        +-----------------+
|  RestaurantApp  |        |  Waiter |        |     Command     |        |   OrderSystem   |
|     (Client)    |        |(Invoker)|        |   (Interface)   |        |    (Receiver)   |
+--------+--------+        +----+----+        +--------+--------+        +--------+--------+
         |                      |                      ^                          ^
         | 1. setCommand(cmd)   |                      |                          |
         +--------------------->| 2. executeCommand()  |                          |
         |                      +--------------------->|                          |
         |                                             |                          |
         |                                             |    3. execute() 메서드     |
         |                                             +-----------+--------------+
         |                                                         |
         |                                                         |
         |                                              +----------v-----------+
         |                                              |   ConcreteCommand    |
         |                                              | (implements Command) |
         |                                              +----------+-----------+
         |                                                         |
         |                                             4. orderSystem.placeOrder()
         |                                                         |
         |                                              +----------v-----------+
         |                                              |     OrderSystem      |
         |                                              |      (Receiver)      |
         |                                              +----------------------+
  • RestaurantApp (Client): 명령 객체를 생성하고 Waiter에게 전달한다.
  • Waiter (Invoker): Command 객체를 보관하고 있다가 executeCommand()로 실행한다.
  • Command (Interface): 명령을 실행하기 위한 인터페이스다.
  • ConcreteCommand (OrderCommand 등): Command 인터페이스를 구현하며, 실행에 필요한 정보를 캡슐화하고 있다.
  • OrderSystem (Receiver): 실제 작업을 수행하는 클래스다.

 

6. Command 패턴 미적용 코드와의 비교

Command 패턴을 사용하면 기능 추가가 용이하다.

 

패턴 미적용 코드를 먼저 알아보자

  • Command 패턴을 사용하지 않을 때 새로운 기능을 추가하려면 OrderSystem과 RestaurantApp 모두를 수정해야 한다.
  • 아래 코드는 기존 OrderSystem 클래스에 modifyOrder() 메서드를 추가하여 주문 수정 기능을 구현했다. 이는 기존 클래스의 코드를 수정하는 것으로, 코드의 안정성을 해칠 수 있다.
// OrderSystem 클래스: 주문 처리 담당
public class OrderSystem {

    public void placeOrder(String item) {
        System.out.println(item + " 주문이 접수되었습니다.");
    }

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

    // 새로운 메서드 추가: 주문 수정 기능
    public void modifyOrder(String oldItem, String newItem) {
        cancelOrder(oldItem);
        placeOrder(newItem);
        System.out.println(oldItem + " 주문이 " + newItem + "(으)로 수정되었습니다.");
    }
    
}

RestaurantApp 클래스 수정

  • RestaurantApp 클래스에서도 위에서 새로 선언한 orderSystem.modifyOrder()를 호출하여 주문 수정 기능을 사용하게 된다. 이 역시 기존 코드를 수정하는 것이며, 클래스 간 결합도가 높아진다.
// RestaurantApp 클래스: 메인 애플리케이션
public class RestaurantApp {

    public static void main(String[] args) {
        OrderSystem orderSystem = new OrderSystem();

        // 기존 코드
        orderSystem.placeOrder("피자");
        orderSystem.placeOrder("파스타");
        orderSystem.cancelOrder("피자");

        // 새로운 기능 사용: 주문 수정
        orderSystem.modifyOrder("파스타", "리조또");
    }
    
}

문제점은 다음과 같다.

  1. 높은 결합도: RestaurantApp이 OrderSystem의 내부 구현에 직접 의존한다.
  2. 유지보수 어려움: 새로운 기능을 추가할 때마다 여러 클래스를 수정해야 한다.
  3. 확장성 부족: 코드 수정으로 인한 부작용이 발생할 수 있다.

 

 

그럼 command 패턴을 적용시키면 뭐가 다를까?



ModifyOrderCommand(명령) 클래스 추가

  • 새로운 명령 클래스 ModifyOrderCommand를 생성하여 Command 인터페이스를 구현한다.
  • 주문 수정에 필요한 정보(oldItem, newItem)와 동작을 execute() 메서드에 캡슐화한다.
  • 이렇게 되면 기존 클래스(OrderSystem, Waiter, RestaurantApp)는 수정할 필요가 없다.
  • 새로운 command(명령) 객체를 생성해서 client에서 사용하면 된다. 생성은 있어도 기존 코드의 수정은 발생하지 않는다.
// ModifyOrderCommand 클래스: 주문 수정 명령을 나타냄
public class ModifyOrderCommand implements Command {

    private OrderSystem orderSystem;
    private String oldItem;
    private String newItem;

    public ModifyOrderCommand(OrderSystem orderSystem, String oldItem, String newItem) {
        this.orderSystem = orderSystem;
        this.oldItem = oldItem;
        this.newItem = newItem;
    }

    @Override
    public void execute() {
        orderSystem.cancelOrder(oldItem);
        orderSystem.placeOrder(newItem);
        System.out.println(oldItem + " 주문이 " + newItem + "(으)로 수정되었습니다.");
    }
    
}

RestaurantApp 클래스에서 새로운 명령(command)을 사용하면 된다.

  • ModifyOrderCommand(명령) 객체를 생성하고 Waiter에게 전달하여 실행한다.
  • 기존 코드의 수정 없이도 새로운 기능을 추가하고 사용할 수 있다.
  • Waiter는 여전히 Command 인터페이스만 알고 있으므로, 명령의 종류에 영향을 받지 않는다. 어떤 명령이든 Command 인터페이스의 구현체이면 다 받아서 사용할 수 있다.
// RestaurantApp 클래스: 메인 애플리케이션
public class RestaurantApp {

    public static void main(String[] args) {
        // 기존 객체 생성
        OrderSystem orderSystem = new OrderSystem();
        Waiter waiter = new Waiter();

        // 기존 명령 실행
        Command pizzaOrder = new OrderCommand(orderSystem, "피자");
        waiter.setCommand(pizzaOrder);
        waiter.executeCommand();

        Command pastaOrder = new OrderCommand(orderSystem, "파스타");
        waiter.setCommand(pastaOrder);
        waiter.executeCommand();

        Command cancelPizzaOrder = new CancelOrderCommand(orderSystem, "피자");
        waiter.setCommand(cancelPizzaOrder);
        waiter.executeCommand();

        // 새로운 명령 실행: 주문 수정
        Command modifyOrder = new ModifyOrderCommand(orderSystem, "파스타", "리조또");
        waiter.setCommand(modifyOrder);
        waiter.executeCommand();
    }
    
}

실행 결과

피자 주문이 접수되었습니다.
파스타 주문이 접수되었습니다.
피자 주문이 취소되었습니다.
파스타 주문이 리조또(으)로 수정되었습니다.

장점

  • 기존 코드 수정 불필요: 새로운 기능을 추가하기 위해 기존 클래스들을 수정하지 않아도 된다.
  • 확장성 향상: 새로운 명령을 추가하는 것이 용이하며, 시스템의 다른 부분에 영향을 주지 않는다.
  • 낮은 결합도 유지: Invoker(Waiter)와 Receiver(OrderSystem) 간의 결합도가 낮아진다.
  • 유지보수성 향상: 새로운 기능이 추가될 때마다 기존 코드의 안정성을 해치지 않는다.

 

비교 요약

  • Command 패턴 미적용 코드
    • 새로운 기능 추가 시 여러 클래스를 수정해야 하며, 이는 결합도를 높이고 유지보수를 어렵게 만든다.

  • Command 패턴 적용 코드
    • 새로운 명령 클래스를 추가하는 것만으로 기능을 확장할 수 있으며, 기존 코드를 수정할 필요가 없어 안정성이 높아진다.

 

결론적으로 Command 패턴을 사용하는 이유는 다음과 같다.

  • Command 패턴을 적용하면 시스템의 확장성과 유지보수성이 크게 향상된다. 새로운 기능을 추가할 때 기존 코드를 수정하지 않고도 쉽게 확장할 수 있으며, 이는 코드의 안정성과 품질을 높인다.

  • 패턴 미적용 코드: 새로운 기능 추가 시 기존 클래스 수정 필요 → 높은 결합도와 유지보수 어려움
  • 패턴 적용 코드: 새로운 명령 클래스 추가로 기능 확장 → 낮은 결합도와 유지보수 용이성

 

 

반응형