07.도메인 서비스

도메인 주도 개발 시작하기 7장을 요약한 내용입니다.

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

도메인 영역 코드를 작성하다 보면, 한 애그리거트로 기능 구현이 어려울 때가 있다.

대표적으로 결제 금액 계산 로직은 아래와 같은 내용이 필요하다.

  • 상품 애그리거트: 구매하는 상품의 가격 필요. 상품에 따라 배송비가 추가

  • 주문 애그리거트: 상품별로 구매 개수 필요

  • 할인 쿠폰 애그리거트: 쿠폰별로 지정한 할인 금액이나 비율에 따라 주문 총 금액 할인. 할인 쿠폰을 조건에 따라 중복 사용하거나 지정 카테고리 상품에만 적용 가능한 제약 조건이 있다면 할인 계산이 복잡

  • 회원 애그리거트: 회원 등급에 따라 추가 할인 가능

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

주문 애그리거트가 필요한 데이터를 모두 가지도록 한 뒤 할인 금액 계산 책임을 주문 애그리거트에 할당해 보자.

  • calculatePayAmount()

    • 쿠폰별로 할인 금액 구하기

    • 회언에 따른 추가 할인 구하기

    • 실제 결제 금액 계산

    • ...

  • calculateDiscount(Coupon coupon)

    • orderLines 각 상품에 대해 쿠폰을 적용해서 할인 금액 계산

    • 쿠폰 적용 조건 등을 확인

    • 정책에 따라 복잡한 조건문 계산

    • ...

  • calculateDiscount(MemberGrade grade)

    • 등급에 따라 할인 금액 계산

    • ...

한 애그리거트에 넣기 애매한 도메인 기능을 억지로 특정 애그리거트에 구현하면 안 된다.

  • 억지로 구현하면 애그리거트는 자신의 책임 범위를 넘어서는 기능을 구현하므로 코드가 길어지고, 외부에 대한 의존이 높아지고, 코드를 복잡하게 만들어 수정을 어렵게 만든다.

  • 추가로 애그리거트의 범위를 넘어서는 도메인 개념이 애그리거트에 숨어들어 명시적으로 드러나지 않게 된다.

도메인 기능을 별도 서비스로 구현해 보자.

도메인 서비스

도메인 서비스도메인 영역에 위치한 도메인 로직을 표현할 때 사용한다.

  • 계산 로직: 여러 애그리거트가 필요한 계산 로직이나, 한 애그리거트에 넣기 다소 복잡한 계산 로직

  • 외부 시스템 연동이 필요한 도메인: 구현하기 위해 타 시스템을 사용해야 하는 도메인 로직

계산 로직과 도메인 서비스

한 애그리거트에 넣기 애매한 도메인 개념을 구현하려면 애그리거트에 억지로 넣기 보다 도메인 서비스를 이용해서 도메인 개념을 명시적으로 드러내자.

  • 응용 영역의 서비스가 응용 로직을 다룬다면 도메인 서비스는 도메인 로직을 다룬다.

도메인 영역의 애그리거트나 밸류와 같은 구성 요소와 다르게 도메인 서비스는 상태 없이 로직만 구현한다.

  • 도메인 서비스를 구현하는 데 필요한 상태는 다른 방법으로 전달받는다.

아래와 같이 도메인의 의미자 드러나는 용어를 타입과 메서드 이름으로 갖는다.

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 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 애그리거트 객체에 도메인 서비스 전달
		order.calculateAmounts(this.discountCalculationService, member.getGrade());
		return order;
	}
	...
}

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

애그리거트 메서드를 실행할 때 도메인 서비스 객체를 파라미터로 전달한다는 것은 애그리거트가 도메인 서비스에 의존한다는 것을 의미한다.

도메인 객체는 필드(프로퍼티)로 구성된 데이터와 메서드를 이용해서 개념적으로 하나인 모델을 표현한다.

  • 모델의 데이터를 담는 필드는 모델에서 중요한 구성요소이다.

  • 하지만, 도메인 서비스 객체 필드는 데이터 자체와는 관련이 없다. 저장 대상도 아니다.

  • 또, 애그리거트가 제공하는 모든 기능에서 도메인 서비스를 필요로 하지 않고, 일부 기능만 필요로 한다.

  • 일부 기능을 위해 도메인 서비스 객체를 애그리거트에 의존 주입할 이유는 없다.

  • 피레임워크의 기능을 사용하고 싶은 개발자의 욕심을 채우는 것에 불과하다.

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

  • ex. 계좌 이체 기능

  • 계좌 이체는 두 계좌 애그리거트가 관여하는데 한 애그리거트는 금액을 출금하고 다른 애그리거트는 금액을 입금

public class TransferService {
	public void transfer(Account fromAcc, Account toAcc, Money amounts) {
		fromAcc.withdraw(amounts);
		toAcc.credit(amounts);
	}
}

응용 서비스는 두 Account 애그리거트를 구한 뒤 해당 도메인 영역의 TransferService를 이용해서 계좌 이체 도메인 기능을 실행

  • 트랜잭션 처리와 같은 로직은 응용 로직이므로 도메인 서비스가 아닌 응용 서비스에서 처리하자.

특정 기능이 응용 서비스인지 도메인 서비스인지 감을 잡기 어려울 경우 해당 로직이 애그리거트의 상태를 변경하거나 애그리거트의 상태 값을 계산하는지 검사해 보자.

Logic 1. 계좌 이체 로직은 계좌 애그리거트의 상태를 변경 Logic 2. 결제 금액 로직은 주문 애그리거트의 주문 금액을 계산

이 두 로직은 각각 애그리거트를 변경하고 애그리거트의 값을 계산하는 도메인 로직이다. 도메인 로직이면서 한 애그리거트에 넣기 적합하지 않으므로 이 두 로직은 도메인 서비스로 구현해야 한다.

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

외부 시스템이나 타 도메인과의 연동 기능도 도메인 서비스가 될 수 있다.

시스템 간 연동은 HTTP API 호출로 이루어질 수 있지만, 설문 조사 도메인 입장에서는 사용자가 설문 조사 생성 권한을 가졌는지 확인하는 도메인 로직으로 볼 수 있다.

  • 역할 관리 시스템과 연동한다는 관점으로 인터페이스를 작성하지 않고 도메인 로직 관점에서 인터페이스를 작성해 보자.

  • SurveyPermisstionChecker 구현 클래스는 인프라스트럭처 영역에 위치해서 연동을 포함한 권한 검사 기능을 구현

public interface SurveyPermisstionChecker {
    boolean hasUserCreationPermission(String userId);
}

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

public class CreateSurveyService {
    private SurveyPermisstionChecker permissionChecker;

    public Long createSurvey(CreateSurveyRequest req) {
        validate(req);
        // 도메인 서비스를 이용해서 외부 시스템 연동을 표현
        if (!permissionChecker.hasUserCreationPermission(req.getRequestorId())) {
            throw new NoPermissionException();
        }
        ...
    }
}

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

도메인 서비스는 도메인 로직을 표현하므로 도메인 서비스의 위치는 다른 도메인 구성 요소와 동일한 패키지에 위치

  • ex. 주문 금액 계산을 위한 도메인 서비스는 주문 애그리거트와 같은 패키지에 위치

├── order
   ├── application
      └── service
          └── OrderService
   └── domain
       ├── model
          └── Order
       ├── service
          └── DiscountCalculationService
       └── repository
└──         └── OrderRepository

도메인 서비스의 개수가 많거나 엔티티와 밸류와 같은 다른 구성요소와 명시적으로 구분하고 싶다면, domain 패키지 밑에 domain.model, domail.service, domain.repository와 같이 하위 패키지를 구분하여 위치시켜도 된다.

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

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

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

  • 도메인 영역에는 도메인 서비스 인터페이스가 위치하고 실제 구현은 인프라스트럭처 영역에 위치하게 된다.

├── order
   ├── domain
      └── Order
      └── DiscountCalculationService
   └── infra
       └── RuleBasedDiscountCalculationService
└──         

도메인 서비스의 구현이 특정 구현 기술에 의존하거나 외부 시스템 API를 실행한다면 도메인 영역의 도메인 서비스는 인터페이스로 추상화해야 한다.

  • 도메인 영역이 특정 구현에 종속되는 것을 방지할 수 있고 도메인 영역에 대한 테스트가 쉬워진다.

예제 프로젝트 패키지 정보

ddd-start2

├── java.com.myshop
   ├── admin
      └── ui
          └── xxxController
   ├── board
      └── domain
   ├── catalog
      ├── command
         └── domain
             ├── category
             └── product
      ├── query
         └── domain
             ├── category
             └── product
      └── ui
   ├── common
      ├── event
      ├── jpa
      ├── model
      └── ui
   ├── eventstore
      ├── api
      ├── infra
      └── ui
   ├── integration
      └── infra
   ├── lock
   ├── member 
       ...
   ├── order
      ├── command
         └── application
         └── domain
      ├── infra
         └── domain
         └── paygate
      ├── query
         └── application
         └── dao
         └── dto
      └── ui
   └── springconfig
       ├── security
       └── web
└──

Last updated