본문 바로가기

책/도메인 주도 개발 시작하기 : DDD 핵심 개념 정리부터 구현까지

Chap 7. 도메인 서비스

들어가며

이 부분은 내용은 짧지만 내용을 한 번에 이해하기 어려웠습니다…ㅠ


여러 애그리거트가 필요한 기능일 때는..

  • “도메인 서비스”를 이용한다.

🤔상황 :

  • 결제 시 필요한 애그리거트는 상품, 주문, 할인, 회원 등이 있다. 이때, 할인 부분에서 쿠폰 할인, 등급 할인, 시즌 할인 등 다양한 조건들이 있다.
  • 결제 금액 계산 로직을 한 애그리거트로 구현할 수 없다고 해서 억지로 주문 애그리거트에 넣는다면?

❌문제 :

  • 코드가 길어지고
  • 외부에 대한 의존이 높아지게 되며
  • 코드를 복잡하게 만들어 수정을 어렵게 만드는 요인
  • 도메인의 개념이 애그리거트에 숨어들어 명시적으로 드러나지 않는다.

⇒ “할인 금액 계산 로직을 위한 도메인 서비스”를 별도로 구현!그러면 다음과 같이 도메인 서비스를 구현해볼 수 있다.

  • public class DiscountCalcalculationService { public Money calculationDiscountAmounts( List<OrderLine> orderLines, List<Coupon> coupons, MemberGrade grade) { Money couponDiscount = coupons.stream() .map(coupon -> calculationDiscount(coupon)) .reduce(Money(0), (v1, v2) -> v1.add(v2)); // 1) 쿠폰 할인 적용 Money membershipDiscount = calculationDiscount(Orderer.getMember().getGrade()); // 2) 멤버쉽 등급 할인 적용 return couponDiscount.add(membershipDiscount); } private Money calculationDiscount(Coupon coupon){ } private Money calculateDiscount(MemberGrade grade){ } }
  • 할인 금액을 계산할 때 쿠폰 할인, 등급 별 할인 2가지가 있다고 하자
  • 애그리거트자신의 책임 범위를 넘어서는 기능을 구현함으로 인한 여러 가지 문제 발생
  • [ EX) 할인 금액 계산 로직을 위한 도메인 서비스 ]

도메인 서비스?

  • 도메인 서비스는 도메인 모델과 밀접하게 관련된 서비스입니다. 기본적으로 비즈니스 로직들은 Entity나 Value Object에 담기는 것을 권장하지만, 특정 객체가 책임을 가지기 애매한 경우가 있는데 이 때, Domain Service를 사용합니다. 도메인 모델에서 처리하기 어려운 로직이나 여러 엔티티나 값 객체를 건너서 처리해야 하는 경우에 사용됩니다. 즉, 하나의 aggregate만으로 처리가 힘든 경우의 ‘도메인 로직' 을 처리해주는 ‘행위’만 가진 서비스라고 할 수 있습니다.
    • 사용 경우
        1. 계산 로직 : 여러 애그리거트가 필요한 계산 로직이나, 한 애그리거트에 넣기에는 다소 복잡한 계산 로직
        1. 외부 시스템 연동이 필요한 도메인 로직 : 구현하기 위해 타 시스템을 사용해야 하는 도메인 로직
  • 다른 도메인 서비스 케이스
    • 주문을 취소한 후에 발생해야 하는 일들이 있지만(쿠폰 돌려받기, 주문 취소 메시지 전송 등), 이 기능들을 order 안에 구현하기에는 너무 과도하다는 생각이 들 때, 이 기능들을 Application Layer에서 순차적으로 호출한다는 생각을 할 수 있습니다.(cancel → reviveCoupon → sendMessage)
    • 하지만 어떤 메서드들이 순차적으로 호출되어야 한다는 것 자체가 도메인 로직에 해당하기 때문에, Application Layer에서 분리되어야 합니다. 이를 도메인 서비스에서 해결할 수 있습니다.
  • 사용 주체 :
    • 할인 계산 서비스를 사용하는 주체는 애그리거트가 될 수 있고 응용 서비스가 될 수 있다.
      1. 애그거트가 사용 주체일 경우
      • DiscountCalculationService를 애그리거트의 결제 금액 계산 기능에 전달
public class Order { // 사용 주체가 애그리거트임
	public void calculateAmounts(
			***DiscountCalcalculationService disCalsvc***, 
			MemberGrade grade
	){
		Money totalAmounts = getTotalAmounts();
		Money discountAmounts = 
				disCalsvc.calculationDiscountAmounts(this.orderLines, this.coupons,grade);
		this.paymentAmounts = totalAmounts.minus(discountAmounts);
	}
}
  • 참고 : 도메인 서비스 객체를 애그리거트에 주입하지 않기
    • 모델의 데이터를 담는 필드는 모델에서 중요한 구성 요소이다.
    • 그런데 discountCalcalculationService 필드는 데이터 자체와는 관련이 없기 때문
    •  

2. 응용 서비스가 사용 주체일 경우

  • 애그리거트 객체에 도메인 서비스를 전달하는 것은 응용 서비스의 책임이다.
public class OrderService {
		**private DiscountCalcalculationService discountCalcalculationService;  //응용서비스의 책임**

	@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.getOrderLines(),
				orderReq.getCoupons(), createOrder(member),
				orderReq.getShippingInfo());
		order.calculationAmounts(***this.discountCalcalculationService***, member.getGrade());
		return order;
	}

}

 


응용 영역의 서비스와 도메인 서비스의 차이 ?

해당 로직인 애그리거트의 상태를 변경하거나 애그리거트의 상태 값을 계산하는지 검사하기! → 이는 도메인 서비스

  • ex) 계좌 이체 로직은 계좌 애그리거트의 상태를 변경 →
    • 결제 금액 로직은 애그리거트의 값을 계산하는 도메인 로직
  • Application 서비스는 로직을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임한다.
public Result doSomeFunc(SomeReq req){
		//1. 리포지터리에서 애그리거트를 구한다. 
		SomeAgg agg = someAggRepository.findById(req.getId());
		checkNull(agg);
		
		//2. 애그리거트의 도메인 기능을 실행한다. 
		agg.doFunc(req.getValue());
		
		//3. 결과를 리턴한다. 
		return createSuccessResult(agg);
	}
  • 계좌 이체 시 도메인 서비스 (도메인 서비스의 기능을 실행할 때 애그리거트를 전달하기도 한다. )
public class TransferService {
	public void transfer(Account fromAcc, Account toAcc, Money amounts){
		fromAcc.withdraw(amounts);
		toAcc.credit(amounts);
	}
}

참고로, 응용 서비스는 사용자 인터페이스나 다른 시스템과의 상호작용을 처리하는 데 사용됩니다. 응용 서비스는 도메인 모델의 논리적인 흐름을 조율하고, 도메인 서비스를 호출하여 작업을 수행합니다. “조율하는 역할”

 

주요 차이점은 도메인 서비스는 도메인 로직을 보유하지만 애플리케이션 서비스는 보유하지 않는다는 것 입니다.

두 번째로는, 트랜잭션 처리를 통해 응용 서비스는 트랜잭션 처리를 담당하여 도메인의 상태 변경을 트랜잭션으로 처리합니다. 하지만, 도메인 모델은 트랜잭션 처리를 하지 않습니다.

그리고 서비스 계층과 다르게 도메인 서비스는 도메인 영역에 위치한다.

 


어노테이션

그렇다면 이렇게 응용 서비스와 도메인 서비스가 다르긴 하지만 @Service 처럼 특정 어노테이션이 있을까?라는 의문이 들었습니다. 그래서 찾아본 결과, Spring에서 Domain Service를 의미하는 annotation은 없다고 합니다.

간혹 Service annotation을 쓰는 경우가 있지만, 이 annotation의 최초 의도는 Business Service Facade로서의 Service로, 주로 Application Layer의 Service를 의미합니다.

팀내 합의가 이루어졌다면 상관없겠지만 개인적으로는 단순 @Component annotation을 사용하거나, custom annotation을 만드는 것을 권장한다고 합니다.

  • 커스텀 어노테이션 예시
@Target({ElementType.TYPE}) 
@Retention(RetentionPolicy.RUNTIME) 
@Component 
public @interface DomainService { }