여러 애그리것이 필요한 기능

  • 도메인 영역 코드를 작성하다 보면 하나의 애그리것으로 기능을 구현할 수 없을 때가 있다.
  • 대표적인 예가 결제 금액 계산 로직
    • 상품 애그리거트 : 구매하는 상품의 가격이 필요하다. 또한 상품에 따라 배송비가 추가되기도 한다.
    • 주문 애그리거트 : 상품별로 구매 개수가 필요하다.
    • 할인 쿠폰 애그리거트 : 쿠폰별로 지정한 할인 금액이나 비율에 따라 주문 총금액을 할인한다.
    • 회원 애그리거트 : 회원 등급에 따라 추가 할인이 가능하다
    • 위 상황에서 실제 결제 금액을 계산하는 주체는 어떤 애그리것일까 ?
      • 총 주문 금액을 계산하는 것은 주문 애그리것이 할수 있지만 총 주문 금액에서 할인 금액을 계산해야 하는데 이 할인금액을 구하는 것은 누구의 책임일까 ?
      • 할인 쿠폰이 규칙을 갖고 있으니 할인 쿠폰의 책임인가 ?
      • 그런데 할인 쿠폰을 두개 이상 적용할 수 있다면 단일 할인 쿠폰 애그리것으로는 총 결제 금액을 계산할 수 없다.
    • 만약 주문 애그리것이 필요한 애그리것이나 필요 데이터를 모두 가지도록 한뒤 할인금액계산을 주문 애그리것에 할당한다면 어떻게 될까 ?
      • 만약 할인정책이 변경되거나 추가된다면 주문애그리것은 할인 애그리것과 관련이 1도 없는데도 불구하고 수정을 해야 한다.
    • 이렇게 한 애그리것에 넣기에 애매한 도메인 기능을 특정 애그리것에서 억지로 구현하면 안된다.
    • 왜냐 이경우 애그리것은 자신의 책임 범위를 넘어서는 구현을 하기 떄문에 코드가 길어지고 외부에 대한 의존이 높아지게 된다.
    • 이는 코드를 복잡하게 만들어 수정을 어렵게 만드는 요인이 된다.
    • 게다가 애그리것의 범위를 넘어서는 도메인 개념이 애그리것에 숨어들어서 명시적으로 드러나지 않게 된다.
    • 이런 경우엔 도메인 서비스 를 사용하자.

도메인 서비스

  • 위처럼 한 애그리것에 넣기 애매한 도메인 개념을 구현하려면 애그리것에 억지로 넣기 보단 도메인 서비스를 이용해서 도메인 개념을 명시적으로 드러내면 된다.
  • 응용 영역의 서비스가 응용 로직을 다룬다면 도메인 서비스는 도메인 로직을 다룬다.
  • 도메인 서비스는 상태 없이 로직만 구현한다
  • 필요한 상태는 애그리것이나 다른 방법으로 전달받는다.
  • 도메인 서비스는 도메인의 의미가 드러나는 용어를 타입과 메서드 이름으로 짓는다.
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);
	}

	...
}

 

  • 할인 계산 서비스를 사용하는 주체는 애그리것이 될 수 있고 응용 서비스가 될수도 있다.
  • 다음과 같이 기능을 전달하면 사용 주체는 애그리것이 된다.
public class Order {
	public void calculateAmounts(
			DiscountCalculationService disCalSvc, MemberGrade grade) {
		Money totalAmounts = getTotalAmounts();
		Money discountAmounts = disCalSvc.calculateDiscountAmounts(this.orderLInes, this.coupons, greade);
		this.paymentAmounts = totalAmounts.minus(discountAmounts);
	}
	...

 

  • 애그리것 객체에 도메인 서비스를 전달하는 것은 응용 서비스 책임이다.
public class OrderService {
	private DiscountCalculationService discountCalculationService;

	@Transactional
	public OrderNo 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.getOrdererId());
		Order order = new Order(orderNo, orderReq.gerOrderLines(),
							orderReq.getCoupons(), createOrderer(member),
							orderReq.getShippingInfo());
		order.calculateAmounts(this.discountCalculationService, member.getGrade());
		return order;
	}
	...
}

 

도메인 서비스 객체를 애그리것에 주입하지 않기.

  • 애그리것의 메소드를 실행할때 도메인 서비스 객체를 파라미터로 전달하는 것은 애그리것이 도메인 서비스에 의존한다는 것을 의미
  • 스프링이 제공하는 의존주입을 사용하여 도메인 서비스를 애그리것에 주입해야 기술적으로 나은것 같은 착각이 들 수 있다.
  • 하지만 개인적 (작가의견) 으론 좋은 방법이 아니라고 한다.
  • 도메인 객체는 필드로 구성된 데이터와 메서드를 이용한 기능을 이용해서 개념적으로 하나인 모델을 표현한다.
  • 모델의 데이터를 담는 필드는 모델에서 중요한 구성요소이다. 그런데 도메인 서비스 필드는 데이터 자체와는 관련이 없다.
  • order 객체를 DB에 보관할때 다른 필드와 달리 저장 대상도 아니다.
  • 또 Order이 제공하는 모든 기능에서 도메인 서비스 ( 할인정책 ) 을 필요로 하는 것도 아니다.
  • 일부 기능만 필요로 하는 도메인서비스 객체를 애그리것에 의존 주입할 필요는 없다.
  • 이는 프레임워크의 기능을 사용하고 싶은 개발자의 욕심을 채우는것에 불과하다.

 

 

  • 도메인 서비스를 인자로 전달하지 않고 반대로 도메인 서비스의 기능을 실행할때 애그리것을 전달하기도 한다.
public class TransgerService {
	public void transfer(Account fromAcc, Account toAcc, Money amounts) {
		fromAcc.withdraw(amounts);
		toAcc.credit(amounts);
	}
}

 

  • 특정기능이 도메인 서비스인지 응용서비스인지 감이 안오는 경우엔 해당 로직이 애그리것의 상태를 변경하거나 애그리것의 상태값을 변경하는지 검사해보면 된다.
  • 도메인 로직이면서 (위와 같이) 한 애그리것에 넣기 적합하지 않은 것은 도메인 서비스로 구현하게 된다.

 

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

  • 도메인 서비스는 도메인 로직을 실행하므로 도메인 서비스의 위치는 다른 도메인 구성요소와 동일한 패키지에 위치한다.
  • 예를 들어 주문금액계산 도메인 서비스는 주문 애그리것과 동일 패키지에 위치한다.
  • 도메인 서비스의 개수가 많거나 엔티티나 밸류와 같은 다른 구성요소와 명시적으로 구분하고 싶다면 domain 패키지 밑에 domain.model, domain.service, domain.repository와 같이 하위 패키지를 구분해서 위치시켜도 된다.

 

 

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

  • 도메인 서비스의 로직이 고정되어 있지 않은 경우 도메인 서비스 자체를 인터페이스로 구현하고 이를 구현한 클래스를 둘 수도 있다.
  • 특히 도메인 로직을 외부 시스템이나 별도 엔진을 이용해서 구현해야 할 경우에 인터페이스와 클래스를 분리하게 된다.

 

출처:https://minkukjo.github.io/dev/2020/11/19/DDD-07/

 

  • 도메인 서비스의 구현이 특정 구현 기술에 의존적이거나 외부 시스템 API를 실행한다면 도메인서비스를 인터페이스로 추상화 해야한다. 이를 통해 도메인 영역이 특정 구현에 종속되는 것을 방지할 수 있고 도메인 영역에 대한 테스트가 수월해진다.

+ Recent posts