실무에서 즐겨 쓰고 있는 GoF 의 디자인 패턴 중, 커맨드 패턴에 대해 작성합니다.
저는 실무에서 CQRS(혹은 CQS) 패턴을 적용하여 CRUD 중 CUD 명령을 처리하는 Command 와 R 에 해당하는 Query 를 명확히 분리하여, 클래스의 단일 책임을 적용하고 응집도를 높이는 방법을 채택했습니다.
이번 글은 CQRS 패턴에 대해 자세히 설명하는 것보다 GoF 의 디자인 패턴 중 커맨드 패턴에 대해 설명드리고자 CQRS 패턴은 다음에 자세히 다루도록 하겠습니다.
CommandHandler
Spring 프레임워크의 내부 코드들을 들여다보면 ~~CommandHandler 라는 클래스가 종종 보입니다.
CommandHandler 는 커맨드 패턴을 활용하여 Command 명령을 처리하는 객체입니다.
즉, CommandHandler 는 명령(Command)을 받아 도메인 로직을 실행하는 역할을 합니다.
커맨드 패턴의 구성요소
- Command Interface : 실행될 명령을 정의
- Concrete Command : 실제 명령을 실행하는 역할을 하는 구체적인 명령 클래스
- CommandHandler(또는 Invoker) : 명령을 실행할 객체를 저장하고 실행하는 역할
- Receiver : 실제 요청을 수행하는 역할
이 구성요소들과 흐름을 코드로 표현하면 아래와 같습니다.
// 상품 도메인 엔티티이자 Receiver
@Entity
public class Product {
private String name;
private String description;
public Product(String name, String description) {
this.name = name;
this.description = description;
}
public static Product newOne(String name, String description) {
return new Product(name, description);
}
}
// Command 인터페이스
public interface Command<T> {
T execute();
}
// Command 인터페이스를 구현한 상품 생성을 위한 Concrete 클래스
public class ProductCreateCommand implements Command<Product>{
private final String name;
private final String description;
public ProductCreateCommand(String name, String description) {
this.name = name;
this.description = description;
}
@Override
public Product execute() {
return Product.newOne(name, description);
}
}
// invoker 역할을 하는 커맨드 핸들러
@Service
public class ProductCommandHandler {
private final ProductCommandRepository repository;
public ProductCommandHandler(ProductCommandRepository repository) {
this.repository = repository;
}
@Transactional
public Product handle(ProductCreateCommand command) {
return repository.save(command.execute());
}
}
CommandHandler 의 handle() 메소드에서 실제 명령을 수행할 구체적인 명령 클래스인 ProductCreateCommand 를 인자로 받아 단순히 실행만 시켜주고 있는 모습입니다.
커맨드 패턴이 유용한 이유
- 이러한 구조로 인해 CommandHandler, Concrete Command 클래스, 도메인 엔티티(Receiver)가 각자의 단일 책임의 원칙(SRP)을 준수합니다.
- 더 나아가 CUD 스펙이 확장된다면, Concrete Command 에 필드만 추가하고 CommandHandler 는 여전히 command.execute() 를 유지하면 되기 때문에 개방 폐쇄 원칙(OCP)도 준수합니다.
다만, 단점으로는 여러개의 Command 클래스가 생길 수 있습니다.
그렇지만 이런 단점보다 장점이 훨씬 크기 때문에 한번 경험해 보시는 것도 좋을 것 같습니다.