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("피자");
}
}
실행 결과
피자 주문이 접수되었습니다.
파스타 주문이 접수되었습니다.
피자 주문이 취소되었습니다.
문제점
- 높은 결합도: RestaurantApp은 OrderSystem의 메서드를 직접 호출하여 두 클래스 간 결합도가 높다.
- 확장성의 한계: 새로운 기능(예: 주문 수정)을 추가하려면 OrderSystem과 RestaurantApp 클래스에 메서드를 추가하거나 수정해야 한다.
- 유연성 부족: 주문 처리를 지연시키거나, 주문 내역을 저장하는 등의 기능을 추가하기 어렵다.
- 유지보수 어려움: 코드가 복잡해질수록 수정과 관리가 어려워진다.
2. Command 패턴 소개
Command 패턴이란?
- Command 패턴은 요청을 객체로 캡슐화하여, 서로 다른 요청에 대해 사용자가 매개변수화를 할 수 있게 해주는 디자인 패턴이다. 이를 통해 요청의 발신자(Invoker)와 수신자(Receiver)를 분리하여 시스템의 유연성과 확장성을 높일 수 있다.
- Command 패턴을 사용하면 다음과 같은 이점을 얻을 수 있다.
- 결합도 감소: 요청의 발신자와 수신자 사이의 결합도를 낮출 수 있다.
- 유연한 요청 처리: 요청을 객체로 표현하여 다양한 방식으로 처리할 수 있다.
- 명령의 캡슐화: 실행에 필요한 모든 정보를 명령 객체에 캡슐화하여 관리할 수 있다.
Command 패턴의 구성 요소 (클래스)
- Command 인터페이스
- 모든 커맨드가 구현해야 하는 인터페이스
- 주로 execute() 메서드를 포함
- ConcreteCommand
- Command 인터페이스를 구현하는 실제 클래스
- 특정 Receiver와 작업을 연결
- Receiver
- 실제 작업을 수행하는 클래스
- ConcreteCommand에 의해 호출됨
- Invoker
- Command 객체를 저장하고 실행하는 클래스
- 언제 명령을 실행할지 결정
- Client (main 메서드)
- ConcreteCommand 객체를 생성하고 Receiver를 설정
- ConcreteCommand 객체를 생성하고 Receiver를 설정
3. Command 패턴의 작동 방식
- Client에서 ConcreteCommand 객체를 생성하고, 필요한 데이터를 설정한 후 Receiver 객체를 지정한다.
- 생성된 Command 객체를 Invoker 객체에게 전달한다.
- Invoker 객체는 필요한 시점에 Command 객체의 execute() 메서드를 호출한다.
- 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("파스타", "리조또");
}
}
문제점은 다음과 같다.
- 높은 결합도: RestaurantApp이 OrderSystem의 내부 구현에 직접 의존한다.
- 유지보수 어려움: 새로운 기능을 추가할 때마다 여러 클래스를 수정해야 한다.
- 확장성 부족: 코드 수정으로 인한 부작용이 발생할 수 있다.
그럼 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 패턴을 적용하면 시스템의 확장성과 유지보수성이 크게 향상된다. 새로운 기능을 추가할 때 기존 코드를 수정하지 않고도 쉽게 확장할 수 있으며, 이는 코드의 안정성과 품질을 높인다.
- 패턴 미적용 코드: 새로운 기능 추가 시 기존 클래스 수정 필요 → 높은 결합도와 유지보수 어려움
- 패턴 적용 코드: 새로운 명령 클래스 추가로 기능 확장 → 낮은 결합도와 유지보수 용이성
반응형