개발/디자인 패턴

GoF 디자인 패턴 : 커맨드 패턴 (그런데 CQRS 패턴을 곁들인...)

배워서, 남주자 2025. 2. 5. 00:07

실무에서 즐겨 쓰고 있는 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 클래스가 생길 수 있습니다.

그렇지만 이런 단점보다 장점이 훨씬 크기 때문에 한번 경험해 보시는 것도 좋을 것 같습니다.

 


ref.https://refactoring.guru/design-patterns/command