Chapter 7. 도메인 서비스 - DDD-START/ONLINE-STUDY GitHub Wiki

Chapter 7. 도메인 서비스

7.1. 여러 애그리거트가 필요한 기능

도메인 영역의 코드를 작성하다 보면 한 애그리거트로 기능을 구현할 수 없을 때가 있다.

예: 결제 금액 계산 로직

  • 상품 애그리거트: 구매하는 상품의 가격 필요하며, 상품에 따라 배공비가 추가되기도 함.
  • 주문 애그리거트: 상품별로 구매 개수가 필요함.
  • 할인 애그리거트: 쿠폰별로 지정한 할인 금액이나 비율에 따라 주문 총 금액을 할인한다. 할인 쿠폰을 조건에 따라 중복 사용할 수 있거나, 지정한 카테고리의 상품에만 적용할 수 있다는 제약 조건이 있다면 할인 계산이 복잡해진다.
  • 회원 애그리거트: 회원 등급에 따라 추가 할인이 가능하다.

실제 결제 금액을 계산해야 하는 주체는 어떤 애그리거트일까??

주문 애거리거트가 필요한 애거리거트나 필요 데이터를 모두 가지도록 한뒤 할인 금액 계산 책임을 주문 애거리거트에 할당하는 것이다.

public class Order {
	...
	private Orderer orderer;
	private List<OrderLine> orderLines;
	private List<Coupon> usedCoupons;

	private Money calculatePayAmounts() {
		Money totalAmountes = calculateTotalAmountes();
		// 쿠폰별로 할인 금액을 구한다.
		Money discount = coupons.stream()
					.map(coupon -> calculateDiscount(coupon))									 
                                        .reduce(Money(0), (v1, v2) -> v1.add(v2));
		// 회원에 따른 추가 할인을 구한다.
		Money membershipDiscount = calculateDiscount(orderer.getMember().getGeade());
		// 실제 결제 금액 계산
		return totalAmounts.minus(discount).minus(membershipDiscount);
	}

	private Money calculateDiscount(Coupon coupon) {
		// OrderLines의 각 상품에 대해 쿠폰을 적용해서 할인 금액 계산하는 로직.
		// 쿠폰의 적용 조건 등을 확인하는 코드
		// 정책에 따라 복잡한 if-else와 계산 코드
		...
	}

	private Money calculateDiscount(MemberGrade grade) {
		... // 등급에 따라 할인 금액 계산
	}
}

여기서 고민거리는 결제 금액 계산 로직이 주문 애그리거트의 책임이 맞느냐에 대한 것이다. 주문 애거리거트가 갖고 있는 구성요소와 관련이 없음에도 불구하고 결제 금액 계산 책임이 주문 애그리거트에 있다는 이유로 주문 애그리거트의 코드를 수정해야 한다.

  • 예) 특별 감사 세일로 전 품목에 대해 한 달간 2% 추가 할인 진행

→ 한 애그리거트에 넣기에 애매한 도메인 기능을 특정 애그리거트에 억지로 구현하면 안된다. 아래 3가지가 그 문제점이다.

  • 자신의 책임 범위를 넘어서는 기능 구현.
  • 코드가 길어지고 의존이 높아지게 됨.
  • 애그리거트의 범위를 넘어서는 도메인 개념이 애그리거트에 숨어들어서 명시적으로 드러나지 않음.

이런 문제에 대힌 해소하는 방법은 도메인 서비스를 별도로 구현하는 것이다.

7.2. 도메인 서비스

할인 금액 규칙 계산처럼 한 애그리거트에 넣기 애매한 도메인 개념을 구현하려면 애그리거트에 억지로 넣기보다는 도메인 서비스를 이용해서 도메인 개념을 명시적으로 드러내면 된다.

응용 영역의 서비스가 응용 로직을 다룬다면, 도메인 서비스는 도메인 로직을 다룬다. 도메인 서비스를 구현하는데 필요한 상태는 애그리거트나 다른 방법으로 전달받는다.

할인 금액 계산 로직을 위한 도메인 서비스는 다음과 같이 도메인의 의미가 드러나는 용어타입과 메서드 이름으로 갖는다.

전달: 애그리거트 → 도메인 서비스

public class DiscountCalculationService {
	public Money calculateDiscountAmounts(
		List<OrderLine> orderLines,
		List<Coupon> usedCoupons,
		MemberGrade grade
	) {
		Money couponDiscount = coupons.stream()															 
                                              .map(coupon -> calculateDiscount(coupon))											 
                                              .reduce(Money(0), (v1, v2) -> v1.add(v2));
		Money membershipDiscount = calculateDiscount(orderer.getMember().getGeade());

		return couponDiscount.add(membershipDiscount);
	}

	private Money calculateDiscount(Coupon coupon) {
		...
	}

	private Money calculateDiscount(MemberGrade grade) {
		...
	}
}

할인 계산 서비스를 사용하는 주체는 애그리거트가 될 수 있고 응용 서비스가 될 수도 있다.

DiscountCalculationService를 아래와 같이 애그리거트의 결제 금액 계산 기능에 전달하면 사용 주체는 애그리거트가 된다.

public class Order {
	public void calculateAmounts(
		DiscountCalculationService disCalSvc, 
		MemberGrade grade
	) {
		Money totalAmountes = getTotalAmounts();
		Money discountAmounts = disCalSvc.calculateDiscountAmounts(this.orderLines, this.coupons, grade);
		this.paymentAmounts = totalAmounts.minus(discountAmounts);
	}
...

애그리거트 객체에 도메인 서비스를 전달하는 것은 응용 서비스 책임이다.

public class OrderService {
	private DiscountCalculationService discountCalculationService;

	@Transactional
	public PrderNo placeOrder(OrderRequest orderRequest) {
		OrderNo orderNo = orderRepository.nextId();
		Order order = createOrder(orderNo, orderRequest);
		orderRepository.save(order);
		// 응용 서비스 실행 후 표현 영역에서 필요한 값 리턴
		return orderNo;
	}

	private Order createOrder(
		OrderNo orderNo,
		OrderRequest orderReq
	) {
		Member member = findMember(orderReq.getOrderId());
		Order order = new Order(orderNo,													 
                                        orderReq.getOrderLines(),
					orderReq.getCoupons(),
					createOrderer(member),
					orderReq.getShippingInfo());
		return order;
	}
...

애그리거트 메서드를 실행할 때 도메인 서비스를 인자로 전달하지 않고 반대로 도메인 서비스의 기능을 실행할 때 애그리거트를 전달하기도 한다.

예) 계좌 이체 기능

public class TrasferService {
	public void transfer(
		Account fromAcc,
		Account toAcc,
		Money acounts
	) {
		fromAcc.withdraw(acounts);
		toAcc.withdraw(acounts);
	}
...

도메인 서비스는 도메인 로직을 수행

  • 트랜잭션 처리와 같은 로직은 응용 로직이므로 응용 서비스에서 처리
💡 특정 기능이 응용 서비스인지 도메인 서비스인지 구분 하는 방법

해당 로직이 애그리거트의 상태를 변경하거나 애그리거트의 상태 값을 계산하는지 검사.

  • 계좌 이체 로직: 계좌 애그리거트의 상태를 변경
  • 결제 금액 로직: 주문 애그리거트의 구문 금액 계산

두 로직은 도메인 서비스로 구현.

  • 두 로직은 각각 애그리거트 변경
  • 애그리거트의 값을 계산하는 도메인 로직

7.2.1. 도메인 서비스의 패키지 위치

도메인 서비스의 위치는 다른 도메인 구성 요소와 동일한 패키지에 위치.

도메인 서비스의 개수가 많거나 엔티티나 밸류와 같은 다른 구성요소와 명시적으로 구분하고 싶은 경우 - domain 패키지 아래 위치

  • domain.model
  • domain.service
  • domain.reposiroty

7.2.2. 도메인 서비스의 인터페이스와 클래스

도메인 서비스의 로직이 고정되어 있지 않은 경우, 도메인 서비스를 인터페이스로 구현하고 클래스로 둘 수도 있다.

도메인 로직을 외부 시스템이나 별도 엔진을 이용해서 구현해야 할 경우에 인터페이스와 클래스를 분리하게 된다.

예) 할인 금액 계산 로직

  • 도메인 영역에는 도메인 서비스 인터페이스가 위치
  • 실제 구현은 인프라스트럭처 영엑에 위치

→ 도메인 영역이 특정 구현에 종속되는 것을 방지

→ 도메인 영역에 대한 테스트 수월

⚠️ **GitHub.com Fallback** ⚠️