프로그래밍 노트/도메인 주도 설계(DDD)

[도메인 주도 설계] 도메인 서비스(Domain Service)

깡냉쓰 2023. 8. 16. 17:42
728x90
반응형

도메인 영역의 코드를 작성하다 보면 한 애그리거트로 기능 구현이 불가능할 때가 존재한다.
바로 결제 금액 계산 로직인데, 실 결제 금액을 계산할 때는 아래 정보들이 필요하다.

  • 상품 애그리거트 : 구매하는 상품의 가격 필요
  • 주문 애그리거트 : 상품별 구매 개수 필요
  • 할인 쿠폰 애그리거트 : 쿠푠별로 지정한 할인 금액이나 비율에 따라 주문 총 금액을 할인하는 정보 필요
  • 회원 애그리거트 : 회원 등급에 따른 추가 할인 정보 필요

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

  • if. 주문 애그리거트에서 필요한 데이터를 모두 가지게 한다면? (계산 책임을 주문 애그리거트에 할당)
    • 할인 정책이 변경되었을 때 주문 애그리거트가 갖고 있는 구성 요소와 관련이 없음에도 결제 금액 계산 책임이 주문 애그리거트에 있기 때문에 해당 코드를 수정해야 한다.

한 애그리거트에 넣기 애매한 도메인 기능은 억지로 특정 애그리거트에 구현하면 안된다. 자신의 책임 범위를 넘어서는 기능을 구현하기 때문이다.

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

!! 도메인 기능(결제 금액 계산 로직)을 별도 서비스로 구현하자 !!


도메인 서비스(Domain Service)

  • 도메인 영역에 위치한 도메인 로직을 표현할 때 사용
    • 계산 로직 : 여러 애그리거트가 필요한 계산 로직이나, 한 애그리거트에 넣기에는 다소 복잡한 계산 로직
    • 외부 시스템 연동이 필요한 도메인 로직 : 구현하기 위해 타 시스템을 사용해야 하는 도메인 로직

계산 로직과 도메인 서비스

  • 도메인 서비스를 이용해서 도메인 개념을 명시적으로 드러낸다.
  • 응용 영역의 서비스가 응용 로직을 다룬다면 도메인 서비스는 도메인 로직을 다룬다.
  • 상태 없이 로직만 구현
// 도메인 서비스 구현
class DiscountCalculationService {
  fun calculateDiscountAmounts(
    orderLines: List<OrderLine>,
    coupons: List<Coupon>,
    grade: MemberGrade
  ) {
    val couponDiscount = coupons.stream()
      .map(coupon -> calculateDiscount(coupon))
      .redouce(Money(0), (v1, v2) -> v1.add(v2))

    val membershipDiscount = calculateDiscount(orderer.getMember().getGrade())

    return couponDiscount.add(membershipDiscount)
  }

  private fun calculateDiscount(coupon: Coupon) = ..
  private fun calculateDiscount(grade: MemberGrade) = ..
}

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

// 아래 경우 사용 주체가 애그리거트가 됨
class Order {
  fun calculateAmounts(disCalSvc: DiscountCalculationService, grade: MemberGrade) {
    val totalAmounts = getTotalAmounts()
    // discount 관련 로직을 Order에서 직접 구현하지 않고, 도메인 객체(disCalSvc)를 이용한다.
    val discountAmounts 
        = disCalSvc.calculateDiscountAmounts(this.orderLines, this.coupons, grade)
    this.paymentAmounts = totalAmounts.minus(discountAmounts)
  }
}
  • 애그리거트 객체에 도메인 서비스를 전달하는 것은 응용 서비스 책임이다.

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

// 계좌 이체는 두 계좌 애그리거트가 관여, 한 애그리거트는 출금 / 한 애그리거트는 입금
// 이를 위한 도메인 서비스
class TransferService {
  fun transfer(fromAcc: Account, toAcc: Account, amounts: Money) {
    fromAcc.withdraw(amounts)
    toAcc.credit(amounts)
  }
}
  • 도메인 서비스는 도메인 로직을 수행하지 응용 로직을 수행하지 않는다.
  • 트랜잭션은 응용 로직에서 구현한다.

도메인 서비스 객체를 애그리거트에 주입하지 말자

  • 애그리거트 메서드(Order.calculateAmounts)를 실행할 때 도메인 서비스 객체를 파라미터로 전달한다는 것은 애그리거트가 도메인 서비스에 의존한다는 것을 의미
  • 기술적으로 나은 것 같다는 착각을 할 수 있지만 의존한다고 의존 주입으로 처리하면 안된다.
    • 프레임워크 기능을 사용하고 싶은 개발자의 욕심을 채우는 것에 불과...
class Order {
  // 의존성 주입을 한다면?
  @Autowired
  private discountCalculationService: DiscountCalculationService
}

Why?

  • 도메인 객체는 데이터(프로퍼티) + 메서드를 이용하여 개념적으로 하나의 모델이다.
    • discountCalculationService 필드는 데이터 자체와 관련이 없다.
    • Order 객체를 DB에 보관할 때 다른 필드와 달리 저장 대상도 아니다.
    • Order 가 제공하는 모든 기능에서 discountCalculationService를 필요로 하는 것도 아니다.
      • 일부 기능만 필요로 하기 때문에 굳이 의존 주입할 이유는 없다.

외부 시스템 연동과 도메인 서비스

외부 시스템이나 타 도메인과의 연동 기능도 도메인 서비스가 될 수 있다.
설문 조사 시스템 / 사용자 역할 관리 시스템이 분리되어 있다고 가정

  • 설문조사를 생성할 때, 사용자가 생성 권한을 가진 역할인지 확인하기 위해 역할 관리 시스템과 연동 필요
  • 설무 조사 도메인 입장에서는 사용자가 설문 조사 생성 권한을 가졌는지 확인하는 과정을 도메인 로직으로 볼 수 있음
  • 이 도메인 로직은 아래의 도메인 서비스로 표현가능
// 역할 관리 시스템 연동 관점이 아니라 도메인 로직 관점에서 인터페이스 작성
interface SurveyPermissionChecker {
  fun hasUserCreationPermission(userId: String)
}

응용 서비스는 이 도메인 서비스를 이용해서 생성 권한을 검사

class CreateSurveryService {
  private permissionChecker: SurveyPermissionChecker

  fun createSurvery(req: CreateSurveryRequest): Long {
    validate(req)
    // 도메인 서비스를 이용해서 외부 시스템 연동을 표현
    if (!permissionChecker.hasUserCreationPermission(req.RequestorId)) {
      throw NoPermissionException()
    }
  }
}
  • SurveyPermissionChecker의 구현체는 인프라스트럭쳐 영역에 위치시킨다.

패키지 구조 예시

application
ㄴ OrderService
domain
ㄴ Order
ㄴ OrderRepository\<Interface>
ㄴ DiscountCalculationService\<Interface>
infra
ㄴ RuleBasedDiscountCalculationService (DIP)

출처) 도메인 주도 개발 시작하기 - 최범균

728x90
반응형