애플리케이션 서비스와 도메인 서비스

아엘·2024년 3월 7일
0

배경

도메인과 관련 없는 다른 코드를 통해 널리 확산될 경우 도메인에 관련된 코드를 확인하고 추론하는게 어렵습니다.
그래서 데이터베이스 코드, 외부 의존성등이 섞여있게 되면 자동화 테스트가 어려워집니다.
이래서 대부분 회사에서 웹 애플리케이션을 구축할때 관심사 분리에 힘을 쓰죠.
레이어드 아키텍처로 개발을 하게되면 각 계층에서 특정 측면만 전문적으로 다룹니다. 이러한 레이어드 아키텍처도 경험과 관례를 바탕으로 계층화가 정해졌다고 합니다.

성공적인 아키텍처는 크게 4가지 개념 계층을 나눈다고 합니다.

  • interface
  • application
  • domain
  • infrastructure

application은 얇게 유지하며, 업무 규칙, 지식이 포함되지 않고, 오직 작업을 조정하고 도메인 계층에 위임합니다.

domain은 업무 개념과 업무에 관한 정보, 규칙을 표현하는 일을 책임집니다. 업무 상황을 반영하는 상태를 제어하고 사용하며 상태 저장과 관련된 기술적인 세부사항은 infrastructure에 위임합니다.

DDD에서 제일 유명한 책인 "도메인 주도 설계" 나온 내용입니다.
책에 나온 다이어그램을 보면 interface -> application -> domain -> infrastructure의 방향으로 흐름이 이어지지, 모든 레이어를 꼭 만들어야 한다고는 얘기하지 않습니다.
어디에도 단순 조회임에도 domain 계층에 서비스를 만들고 repository를 호출하도록 하라고 써있지 않습니다.

DDD가 워낙 유명해서 application, domain 레이어는 거진 만들어서 개발합니다. 그러나 대부분 잘 모르고 할겁니다. 제가 그렇습니다.


서비스

Entity, Value Object에서 찾지 못하는 중요한 도메인 연산이 있습니다. 이들 중 일부는 본질적으로 사물이 아닌 활동이나 행동인데, 모델에는 어울리지 않는 것들이면서 필요한 도메인 기능을 위해 서비스라는 모델에서 독립적인 인터페이스로 Entity와 Value Object와 달리 상태를 캡슐화 하지 않는다고 합니다. "Service는 흔히 사용되는 패턴이지만, 도메인 계층에서도 마찬가지로 적용될 수 있다." 라고 합니다.

Service 라는 이름은 다른 객체와의 관계를 강조하며, Entity, Value Object와 달리 순전히 클라이언트에 무엇을 제공할 수 있느냐를 책임집니다. 그래서 Entity가 명사로 이름을 부여하는것과 달리 Service는 주로 액션으로 이름을 짓습니다. "Service의 연산의 명칭은 유비쿼터스 언어에서 유래되거나 도입돼야 한다, 라고하며, Service의 결과는 도메인 객체여야 한다." 라고 합니다.

Service는 Entity, Value Object의 행위를 모두 가져와서는 안되며, 어떤 연산이 중요한 도메인 개념이면 Service에 작성하고, 상태를 갖지 않게 만들라고 합니다.

그리고 Service는 도메인 계층에만 이용되는 것은 아니며, 도메인 계층의 Service와 다른 계층에 속한것들을 분명히 구분하고, 그러한 구분을 분명하게 유지하는 책임을 나누는데 주의를 기울이라고 합니다.

도메인 서비스와 애플리케이션 서비스 구분

응용 Service, 도메인 service 구분하는 것은 어렵다고 책에도 나옵니다. 여기서부터는 제가 경험하고, 해석한 내용입니다. 애플리케이션 서비스는 애플리케이션 사용자가 사용하는 행위입니다. 그러나 백엔드 개발자 입장에서는 그 서비스를 제공하기위해 도메인 서비스 ( 우리 서비스에서 지원하는 기능 ) 와 외부 서비스와의 조합으로 사용자에게 서비스를 제공할겁니다. 또는 외부 서비스가 아닌 다른 도메인 서비스에게 위임하여 비즈니스 요구사항을 만족시킬겁니다.

udemy강의를 듣다가 더 좋은 방법이 구분 점에 대해 언급되어 추가 공유합니다.

도메인서비스는 여러 애그리거트에 걸쳐있는 비즈니스 로직을 조정해야합니다.
비즈니스 로직이 어떤 엔티티에도 적합하지 않을 수 있습니다. 그런 로직들을 도메인서비스에 담습니다.
그리고 도메인 로직을 외부와 통신하기 위해서는 애플리케이션 서비스가 필요합니다.

코드 예시

애플리케이션 서비스

애플리케이션에서는 도메인서비스에게 위임하는 작업 이외에도 작업을 수행합니다.

public class OrderApplicationServiceImpl implements OrderApplicationService {

    private final OrderCreateCommandHandler orderCreateCommandHandler;

    private final OrderTrackCommandHandler orderTrackCommandHandler;

    public OrderApplicationServiceImpl(OrderCreateCommandHandler orderCreateCommandHandler, OrderTrackCommandHandler orderTrackCommandHandler) {
        this.orderCreateCommandHandler = orderCreateCommandHandler;
        this.orderTrackCommandHandler = orderTrackCommandHandler;
    }

    @Override
    public CreateOrderResponse createOrder(CreateOrderCommand createOrderCommand) {
        return orderCreateCommandHandler.createOrder(createOrderCommand);
    }
	...
}
public class OrderCreateCommandHandler {
	...
    public OrderCreateCommandHandler(OrderCreateHelper orderCreateHelper, OrderDataMapper orderDataMapper, OrderSagaHelper orderSagaHelper, PaymentOutboxHelper paymentOutboxHelper) {
        this.orderCreateHelper = orderCreateHelper;
        this.orderDataMapper = orderDataMapper;
        this.orderSagaHelper = orderSagaHelper;
        this.paymentOutboxHelper = paymentOutboxHelper;
    }

    @Transactional
    public CreateOrderResponse createOrder(CreateOrderCommand createOrderCommand) {
        OrderCreatedEvent orderCreatedEvent = orderCreateHelper.persistOrder(createOrderCommand);
        log.debug("Order is created with id: {}", orderCreatedEvent.getOrder().getId().getValue());
        CreateOrderResponse createOrderResponse = orderDataMapper.orderToCreateOrderResponse(orderCreatedEvent.getOrder(), "Order created successfully");

        return createOrderResponse;
    }
}

...
public class OrderCreateHelper {

    ...
    @Transactional
    public OrderCreatedEvent persistOrder(CreateOrderCommand createOrderCommand) {
        checkCustomer(createOrderCommand.getCustomerId());
        Restaurant restaurant = checkRestaurant(createOrderCommand);
        Order order = orderDataMapper.createOrderCommandToOrder(createOrderCommand);
        OrderCreatedEvent orderCreatedEvent = orderDomainService.validateAndInitiateOrder(order, restaurant);
        saveOrder(order);
        return orderCreatedEvent;
    }

    private Restaurant checkRestaurant(CreateOrderCommand createOrderCommand) {
        Restaurant restaurant = orderDataMapper.createOrderCommandToRestaurant(createOrderCommand);
        Optional<Restaurant> optionalRestaurant = restaurantExecutor.getRestaurantByIdAndProductIdIn(restaurant);
        if (optionalRestaurant.isEmpty()) {
            log.warn("Could not find restaurant with restaurant id: {}", createOrderCommand.getRestaurantId());
            throw new OrderDomainException("Could not find restaurant with restaurant id: "+ createOrderCommand.getRestaurantId());
        }
        return optionalRestaurant.get();
    }


    private void checkCustomer(UUID customerId) {
        Optional<Customer> customer= customerExecutor.getCustomerBy(customerId);
        if(customer.isEmpty()){
            log.warn("Could not find customer with customer id: {}", customerId);
            throw new OrderDomainException("Could not find customer with customer id: "+customerId);
        }
    }

    private Order saveOrder(Order order) {
        Order orderResult = orderRepository.save(order);
        if( orderResult == null) {
            log.error("Could not save order!");
            throw new OrderDomainException("Could not save order!");
        }
        log.debug("Order is saved with id: {}", orderResult.getId().getValue());
        return orderResult;
    }
}

도메인 서비스도 도메인 객체, 값객체에 위임하지 않은 중요한 도메인 개념을 구현합니다.

public class OrderDomainServiceImpl implements OrderDomainService{

	...
    @Override
    public OrderCreatedEvent validateAndInitiateOrder(Order order,
                                                      Restaurant restaurant
                                                      ) {
        validateRestaurant(restaurant);
        setOrderProductInformation(order, restaurant);
        order.validateOrder();
        order.initOrder();
        log.debug("Order with id: {} is initiated", order.getId().getValue());
        return new OrderCreatedEvent(order, ZonedDateTime.now(ZoneId.of(UTC)));
    }

    private void setOrderProductInformation(Order order, Restaurant restaurant) {
        order.getItems().forEach(orderItem -> restaurant.getProducts().forEach(restaurantProduct -> {
            Product currentProduct = orderItem.getProduct();
            if (currentProduct.equals(restaurantProduct)) {
                currentProduct.updateWithConfirmedNameAndPrice(restaurantProduct.getName(),
                        restaurantProduct.getPrice());
            }
        }));
    }

    private void validateRestaurant(Restaurant restaurant) {
        if(!restaurant.isActive()) {
            throw new OrderDomainException("Restaurant with id " + restaurant.getId().getValue() +
                    " is currently not active!");
        }
    }
    ...
}

결론

애플리케이션 서비스에서 사용자가 원하는 비즈니스 요구사항을 만족할 서비스를 구현합니다.
Entity, Value Object의 행위가 아니지만 도메인의 중요한 핵심 연산이다. -> 도메인 서비스로 추출합니다.
도메인 서비스는 상태를 갖지 않습니다.
애플리케이션은 도메인서비스에 이러한 책임을 위임합니다.

출처:
도메인 주도 설계
Domain services vs Application services: https://enterprisecraftsmanship.com/posts/domain-vs-application-services/

profile
하루 하나씩

0개의 댓글