본문 바로가기

소프트웨어-이야기/프로그래밍 언어와 프레임워크

[DDD] Specification 명세

Specification

Specification 객체는 객체가 특정 조건을 만족하는지 확인하는 역할을 한다. 본 글에서는 Specifiaction을 언제, 왜 사용하는지에 대해서 설명하고자 한다. 

 

유효성 검증 추가 

(1) 도메인에 간단한 유효성 검증 추가하기

도메인에 유효성 검증을 추가해야할 때, 로직이 간단한 경우에는 도메인 엔티티의 메서드로 유효성 검증 함수를 추가하는 방식으로 구현할 수 있다. 그러나 유효성 검증 절차가 복잡한 경우, 도메인 엔티티의 메서드로 두는 것이 어색한 경우가 있다.

 

(2) 서비스에 추가하기

예를 들어, 인당 구매 수량이 제한되는 상품을 판매하는 경우, 주문서를 생성하기 전에 누적 주문 수량을 조회해야한다. 이 경우, 레파지토리를 통해 회원의 누적 주문 수량을 조회해야한다. 때문에 해당 코드는 주문 엔티티 객체를 생성하는 로직에 유효성 검증을 추가하기 난감하다.

때문에 이러한 객체 유효성 검증 로직은 보통 서비스 레이어에 구현된다. 그러나 이는 도메인 규칙 중에도 중요도가 높은 기능이기 때문에 서비스에 추가하는 것을 지양해야한다.

@Service
@AllArgsConstructor
public class OrderServiceImpl implements OrderService{
    private final OrderRepository orderRepository;
    private final SalesProductPriceRepository salesProductPriceRepository;
    
    public void register(OrderRegisterCommand command){
        SalesProductPrice salesProductPrice = salesProductPriceRepository.findBy(command.getProductId());
        int orderQuantity = orderRepository.getQuantityByProductIdAndUserId(salesProductPrice.getProductId(), command.getUserId());
        
        if(orderQuantity > orderQuantityproduct.getPerLimitTotalQuantity()){
            throws new OrderRegisterException("주문 가능한 상품 수량을 초과했습니다.");
        }
        
		...
    }
}

 

서비스에는 도메인 규칙에 근거한 로직이 있으면 안된다. 만약 서비스에 도메인 규칙을 추가하는 것을 허용하면, 서비스 코드에 도메인 규칙이 산재하게 된다. 결국, 도메인 객체가 제 역할을 빼앗기게 된다. 

(3) 도메인 엔티티에 레파지토리 주입하기

도메인 규칙은 도메인 객체에 정의되어야한다. 그러나 위의 예시를 충족하기 위해 엔티티에 레파지토리를 추가할 수는 없기 때문에 새로운 대안이 필요하다. 

@Entity
public class Order {
	...
    public boolean validateQuantity(OrderRepository orderRepository, 
                                    SalesProductPriceRepository salesProductPriceRepository, 
                                    Long userId){
        SalesProductPrice salesProductPrice = salesProductPriceRepository.findBy(command.getProductId());
        int orderQuantity = orderRepository.getQuantityByProductIdAndUserId(salesProductPrice.getProductId(), userId);
        
        if(orderQuantity > orderQuantityproduct.getPerLimitTotalQuantity()){
            throws new OrderRegisterException("주문 가능한 상품 수량을 초과했습니다.");
        }
    }
}

이런 모습이 되면 안된다.

 

(4) Specification 추가하기

specification 객체를 사용하면 위와 같은 문제를 해결할 수 있다.

@Component
@AllArgsConstructor
public class OrderQuantitySpecification implements OrderSpecification{
    private final OrderRepository orderRepository;
    private final SalesProductPriceRepository salesProductPriceRepository;
    
    public void isSatisfiedBy(long productId, long userId){
        SalesProductPrice salesProductPrice = salesProductPriceRepository.findBy(command.getProductId());
        int orderQuantity = orderRepository.getQuantityByProductIdAndUserId(salesProductPrice.getProductId(), user.getId());
       
        return orderQuantity > orderQuantityproduct.getPerLimitTotalQuantity();
    }
}

 

specification은 객체가 조건을 만족하는지 확인하는 역할을 한다. 평가 대상 객체가 복잡한 유효성 검증 코드에 파묻히는 일 없이 원래의 의도를 그대로 잘 드러낼 수 있게 하는 역할을 한다. 

 

@Service
@AllArgsConstructor
public class OrderServiceImpl implements OrderService{
    private final OrderRepository orderRepository;
    private final SalesProductPriceRepository salesProductPriceRepository;
    private final List<OrderSpecification> orderSpecifications;
    
    public void register(OrderRegisterCommand command){
        for(OrderSpecification orderSpecification: orderSpecifications){
            if(!orderSpecification.isSatisfiedBy(command.getProductId(), command.getUserId())){
                throws new OrderRegisterException("주문 가능한 상품 수량을 초과했습니다.");
            }
        }    
        ...
    }
}

서비스 코드에서는 다음과 같이 specification 객체를 사용한다. 

이렇게 하면 객체를 평가하는 유효성 검증 로직을 캡슐화하는 것이 가능하고, 유연하게 유효성 검증 로직을 추가할 수 있다. 

 

객체의 유효성 검증 로직이 해당 객체에만 있어야한다는 법은 없다. 유효성 검증 코드를 명세 같은 객체로 분리하는 방안도 있다. 

 

참고

도메인 주도 설계 철저 입문 - 코드와 패턴으로 밑바닥부터 이해하는 DDD 13장

https://techblog.woowahan.com/2709/

https://medium.com/@pawel_klimek/domain-driven-design-specification-pattern-82867540305c

https://gitlab.com/pawelklimek/ddd-specification/-/tree/master/src/main/java/com/klimek/ddd/patterns/specification/complex/specifications