본 포스팅은 DDD 를 공부하면서 정리하기 위한 포스팅입니다.
출처: 도메인 주도 개발 시작하기 - DDD 핵심 개념 정리부터 구현까지 (저자. 최범균)
1. 도메인 서비스(Domain Service) 란?
비지니스 로직을 개발하다보면, 애그리거트 1가지만 가지곤 개발할 수 없는 기능이 발생할 수 있습니다.
예를 들면, 아래와 같은 기능이 있다고 가정해보겠습니다.
판매처에서 할인쿠폰을 발급해 구매자가 해당 판매처의 상품을 구매시, 쿠폰을 통해 금액을 할인받을 수 있는 기능
위 기능을 개발하기 위해 필요한 애그리거트는 대략 3가지 정도로 생각해볼 수 있습니다.
(물론, 더 디테일하게 고민해보면 더 많아질 수 도, 더 적어질 수 도 있습니다.)
- 주문 애그리거트 : 실제 주문 정보를 생성
- 상품 애그리거트 : 구매하고자하는 상품의 가격을 조회
- 쿠폰 애그리거트 : 사용하고자하는 쿠폰의 사용기한 검증 및 할인율 적용
위 애그리거트 정도가 필요하다고 보여지지만, 가장 중요한 "결제 금액 계산" 을 어떤 애그리거트에서 수행할 지 고민해봐야 합니다.
가장 그럴싸한 위치는 주문 애그리거트 일 수 있겠습니다만, "결제 금액을 계산하는 기능이 주문 애그리거트의 책임인가" 라는 측면에선 많은 고민을 해봐야한다고 생각합니다.
주문 애그리거트가 결제금액을 계산에 대한 책임을 가진다고 생각하고 기능을 구현하면 아래와 같이 구현해볼 수 있습니다.
public class Order {
// ... 다른 필드 중략
private List<OrderItem> orderItems;
private int totalAmounts;
public void calculateTotalAmounts() {
this.totalAmounts = orderItems.stream()
.mapToInt(orderItem -> orderItem.calculatePriceAmounts())
.sum();
}
}
public class OrderItem {
// ... 다른 필드 중략
private Coupon coupon;
private int quantity;
private int productPrice;
public int calculatePriceAmounts() {
return coupon.calculateDiscountAmounts(this.quantity * this.productPrice);
}
}
위 코드를 보면, 정말 이대로 괜찮은가 라는 생각이 들게하는 코드가 있습니다.
바로 주문상품 객체 OrderItem 에 속한 Coupon 이라는 객체입니다. 애그리거트의 구현 측면으로 볼 때, OrderItem은 주문 애그리거트에 속한 하나의 객체이고, Coupon은 쿠폰 애그리거트의 루트 엔티티 입니다.
각각의 서로 다른 애그리거트인데, 주문 애그리거트가 쿠폰 애그리거트를 감싸고 있는 모습인 것 입니다.
애그리거트에 포함되는 객체들을 선별하는 확실한 기준은 "요구사항" 을 수립하는 것이고, 그에 따라 명백해지는 "객체들 간의 동일한 라이프사이클을 가지는가" 를 따지는 것입니다.
하지만, 주문 정보의 라이프사이클과 쿠폰 정보의 라이프사이클은 여러모로 맞지 않을 것 입니다.
주문과 쿠폰의 라이프사이클이 같기 위해선 구매자가 주문을 하면 쿠폰이 발급되어야 한다는 의미인데 이 또한 이상합니다.
즉, 주문과 쿠폰은 서로 다른 독립적인 애그리거트기 때문에 위와 같은 코드 스타일은 DDD 관점에선 맞지 않습니다.
따라서, 주문 애그리거트 루트 엔티티에서 또한 결제 금액 계산 로직 자체를 품기에는 무리가 있습니다.
그렇다면 대체 결제 금액 계산 로직은 어느 애그리거트에서 품어야 할까요?
이렇게 여러 애그리거트가 연관되어 있어 특정 애그리거트에서 비지니스 로직을 품기 어려운 경우, 이를 구현하기 위해 도메인 서비스(Domain Service)가 존재한다고 할 수 있습니다.
2. 도메인 서비스 구현
2.1 여러 애그리거트가 연관된 도메인 로직 구현
도메인 서비스는 여러 애그리거트를 가지고 도메인 로직을 개발하기 위해 사용하므로, 별도의 필드변수를 가지지 않고, 도메인 로직만을 가지는 형태로 구현됩니다.
public class DiscountAmountService {
public void calculateDiscountAmounts(Order order, Coupon coupon) {
// 할인 금액 적용 로직 수행
}
}
파라미터로 여러 애그리거트를 받아 복잡적인 도메인 로직을 구현하게 되는데, 도메인 서비스의 메서드를 호출하고, 파라미터로 애그리거트를 전달하는 것은 응용 서비스의 책임입니다.
public class OrderService {
private OrderRepository orderRepository;
private CouponRepository couponRepository;
private DiscountAmountService discountAmountService;
@Transactional
public OrderId create(OrderCreateDto createDto) {
// 애그리거트 생성 및 조회
Order order = Order.create(createDto.getItems(), ...생략);
Coupon coupon = couponRepository.findById(createDto.getCouponId());
// 도메인 서비스 로직 호출
discountAmountService.calculateDiscountAmount(order, coupon);
// 주문 애그리거트 저장
Order savedOrder = orderRepository.save(order);
// 결과 반환
return savedOrder.getOrderId();
}
}
도메인 서비스는 기본적으로 "도메인 로직"을 구현하는 용도로 사용됩니다. 응용 서비스와 class 이름이 비슷해서 혼동할 수 있지만,
응용 서비스는 "트랜잭션을 관리" 하는 반면, 도메인 서비스는 오로지 "도메인 로직 구현"에 집중해야 합니다.
2.2 외부 시스템과의 연동이 필요한 도메인 로직 구현
외부 시스템과의 연동이나 다른 도메인과의 연동되는 기능이 포함되는 로직이 있다면 이 또한 도메인 서비스에서 구현될 수 있습니다.
예를 들어, 주문 상품의 배송현황을 조회하는 기능이 있다고 가정해보겠습니다.
쇼핑몰의 경우, 현재 배송중인 상품이 어디에 있는지 현황 조회를 위해 직접 배송을 하지 않는 이상 택배회사와의 연동이 필요할 것 입니다.
택배회사에서 사용하는 택배조회시스템은 쇼핑몰 내 구현된 시스템이 아니기 때문에, 해당 택배회사에서 제공해주는 API 를 호출하여 결과를 받아서 사용자에게 보여줘야 하고, 흐름을 간략하게 코드로 본다면 아래처럼 나타날 수 있습니다.
public interface DeliveryStateFinder {
OrderDeliveryStateDto findCurrentDeliveryState(Order order);
}
public class OrderDeliveryService {
private OrderRepository orderRepository;
private DeliveryStateFinder deliveryStateFinder;
public OrderDeliveryStateDto findOrderDeliveryState(OrderId orderId) {
Order order = orderRepository.findById(orderId);
// 현재 배송현황 조회
return deliveryStateFinder.findCurrentDeliveryState(order);
}
}
위 코드에서 짚고 넘어가야 하는 부분은 바로 도메인 서비스를 interface 로 구현한 점 입니다.
외부 시스템과의 API 연계를 도메인 서비스에 구현하는 경우, 연계 대상이 변경될 수 도 있고 개발 시점에서 정해지지 않을 수 도 있습니다.
그리고, 무엇보다 테스트 코드를 작성할 때, 만일 interface 가 아니라 실제 구현체 class 로 구현한다면 테스트 코드가 실행될 때 마다 지속적으로 외부 API를 호출할 것이기 때문에, 상대 시스템에도 부하를 줄 뿐 만 아니라 외부 요인으로 인해 테스트 결과가 달라질 수 있습니다.
interface 로 구현함으로써, 구현기술이 변경되어도(택배 업체가 변경되는 등) 테스트 코드를 작성할 때 도 실제 비지니스 로직을 구현한 응용 서비스에 영향을 주지 않도록 하는 것이 중요합니다.
'DDD&MSA' 카테고리의 다른 글
[DDD] 바운디드 컨텍스트(Bounded Context) (0) | 2023.07.19 |
---|---|
[DDD] 애그리거트 트랜잭션과 Lock 기법 (0) | 2023.06.18 |
[DDD] 응용 서비스 구현과 표현 계층 (0) | 2023.06.07 |
[DDD] 주문 애그리거트 구현하기(with JPA) (0) | 2023.06.03 |
[DDD] 애그리거트(Aggregate) 이해하기 (0) | 2023.06.02 |
댓글