-
[도메인 주도 개발 시작하기] 7. 도메인 서비스STUDY/DDD 2022. 12. 19. 22:16
1. 여러 애그리거트가 필요한 기능
도메인 영역의 코드를 작성하다 보면, 한 애그리거트로 기능을 구현할 수 없을 때가 있다. 대표적인 예로 결제 금액 계산 로직이 있다.
- 상품 애그리거트 : 구매하는 상품의 가격이 필요하다. 또한 상품에 따라 배송비가 추가되기도 한다.
- 주문 애그리거트 : 상품별로 구매 개수가 필요하다.
- 할인 쿠폰 애그리거트 : 쿠폰별로 지정한 할인 금애리나 비율에 따라 주문 총 금액을 할인한다....
- 회원 애그리거트 : 회원 등급에 따라 추가 할인이 가능하다.
총 결제 금액을 계산하기 위한 할인 쿠폰 적용, 추가 할인 적용 등을 하나의(예를 들어, 주문) 애그리거트를 정해 그곳에 모두 구현할 수 있지만, 한 애그리거트에 애매한 도메인 기능을 억지로 구현할 경우 자신의 책임 범위를 넘어서는 기능을 구현하기 때문에 코드가 길어지고 외부에 대한 의존이 높아지게 되며 코드를 복잡하게 만들어 수정을 어렵게 만드는 요인이 된다. 또한 애그리거트의 범위를 넘어서는 도메인 개념이 애그리거트에 숨어들어 명시적으로 드러나지 않게 되는 문제가 발생할 수 있다.
이러한 문제를 해소하는 방법 중 하나가 바로 도메인 기능을 별도 서비스로 구현하는 것이다.
2. 도메인 서비스
도메인 서비스는 도메인 영역에 위치한 도메인 로직을 표현할 때 사용한다.
- 계산 로직 : 여러 애그리거트가 필요한 계산 로직이나, 한 애그리거트에 넣기에는 다소 복잡한 계산 로직
- 외부 시스템 연동이 필요한 도메인 로직 : 구현하기 위해 타 시스템을 사용해야 하는 도메인 로직
할인 금액 규칙 계산처럼 한 애그리거트에 넣기 애매한 도메인 개념을 구현하려면 애그리거트에 억지로 넣기보다는 도메인 서비스를 이용해서 도메인 개념을 명시적으로 드러내면 된다. 응용 영역의 서비스가 응용 로직을 다룬다면 도메인 서비스는 도메인 로직을 다룬다.
도메인 영역의 애그리거트나 밸류와 같은 구성요소와 도메인 서비스를 비교할 때 다른 점은 도메인 서비스는 상태 없이 로직만 구현한다는 점이다. 도메인 서비스를 구현하는 데 필요한 상태는 다른 방법으로 전달받는다.
public class DiscountCalculationService { public Money calculateDiscountAmounts( List<OrderLine> orderLines, List<Coupon> coupons, MemberGrade grade ) { Money couponDiscount = coupons.stream() .map(coupon -< calculateDiscount(coupon)) .reduce(Money(0), (v1, v2) -> v1.add(v2)); Money membershipDiscount = calculateDiscount(orderer.getMember().getGrade()); return couponDiscount.add(membershipDiscount); } private Money calculateDiscount(Coupon coupon) { ... } private Money calculateDiscount(MemberGrade grade) { ... } }
도메인 서비스를 사용하는 주체는 애그리거트가 될 수도 있고 응용 서비스가 될 수도 있다.
public class Order { public void calculateAmounts( // 사용하는 주체가 애그리거트인 경우 DiscountCalculationService service, MemberGrade grade ) { Money totalAmounts = getTotalAmouts(); Money discountAmounts = service.calculateDiscountAmounts(this.orderLines, this.coupons, grade); this,paymentAmounts = totalAmounts.minus(discountAmounts); } ... } public class OrderService { private DiscountCalculationService discountCalculationService; ... private Order createOrder(OrderNo orderNo, OrderRequest orderRequest) { Member member = ...; Order order = ...; ... // 응용 서비스에서 애그리거트 객체에 도메인 서비스 전달 order.calculateAmounts(this.discountCalculationService, member.getGrade()); } }
애그리거트 메서드를 실행할 때 도메인 서비스를 인자로 전달하지 않고 반대로 도메인 서비스의 기능을 실행할 때 애그리거트를 전달하기도 한다.
public class TransferService { public void transfer(Account fromAcc, Account toAcc, Money amounts) { ... } }
도메인 서비스는 도메인 로직을 수행하지 응용 로직을 수행하진 않는다. 트랜잭션 처리와 같은 로직은 응용 로직이므로 도메인 서비스가 아닌 응용 서비스에서 처리해야 한다.
외부 시스템이나 타 도메인과의 연동 기능도 도메인 서비스가 될 수 있다. 예를 들어 설문 조사 시스템과 사용자 역할 관리 시스템이 분리되어 있다고 가정했을 때, 설문 조사 시스템은 설문 조사를 생성할 때 사용자가 생성 권한을 가진 역할인지 확인하기 위해 역할 관리 시스템과 연동해야 한다.
시스템 간 연동은 HTTP API 호출로 이루어질 수 있지만 설문 조사 도메인 입장에서는 사용자가 설문 조사 생성 권한을 가졌는지 확인하는 도메인 로직으로 볼 수 있다. 이러한 도메인 로직은 도메인 서비스로 표현할 수 있고, 응용 서비스는 이 도메인 서비스를 이용해서 생성 권한을 검사한다. 도메인 서비스는 도메인 로직 관점에서 인터페이스를 작성하고, 구현 클래스는 인프라스트럭처 영역에 위치해 연동을 포함한 권한 검사 기능을 구현한다.
public interface SurveyPermissionChecker { boolean hasUserCreationPermission(String userId); } public class CreateSurveyService { private ServeyPermissionChecker permissionChecker; public Long createSurvey(CreateSurveyRequest req) { validate(req); // 도메인 서비스를 이용해서 외부 시스템 연동을 표현 if(!permissionChecker.hasUserCreationPermission(req.getRequestorId())) { ... } } }
도메인 서비스는 도메인 로직을 표현하므로 도메인 서비스의 위치는 다른 도메인 구성요소와 동일한 패키지에 위치한다.
도메인 서비스의 로직이 고정되어 있지 않은 경우 도메인 서비스 자체를 인터페이스로 구현하고 이를 구현한 클래스를 둘 수도 있다. 특히 도메인 로직을 외부 시스템이나 별도 엔진을 이용해서 구현할 때 인터페이스와 클래스를 분리하게 된다.
도메인 서비스의 구현이 특정 구현 기술에 의존하거나 외부 시스템의 API를 실행한다면 도메인 영역의 도메인 서비스는 인터페이스로 추상화해야 한다. 이를 통해 도메인 영역이 특정 구현에 종속되는 것을 방지할 수 있고 도메인 영역에 대한 테스트가 쉬워진다.
출처 : 최범균, 『도메인 주도 개발 시작하기 DDD 핵심 개념 정리부터 구현까지』, 한빛미디어(2022), p234-p243
'STUDY > DDD' 카테고리의 다른 글
[도메인 주도 개발 시작하기] 9. 도메인 모델과 바운디드 컨텍스트 (1) 2022.12.27 [도메인 주도 개발 시작하기] 8. 애그리거트 트랜잭션 관리 (0) 2022.12.19 [도메인 주도 개발 시작하기] 6. 응용 서비스와 표현 영역 (0) 2022.12.12 [도메인 주도 개발 시작하기] 4. 리포지터리와 모델 구현 (0) 2022.12.01 [도메인 주도 개발 시작하기] 3. 애그리거트 (0) 2022.11.18