여러 애그리거트가 필요한 기능
도메인 영역의 코드를 작성하다 보면, 한 애그리거트로 기능을 구현할 수 없는 경우가 있습니다.
대표적으로, 결제 금액 계산 로직이 있는데 다음과 같은 내용이 필요합니다.
- 상품 애그리거트
- 주문 애그리거트
- 할인 쿠폰 애그리거트
- 회원 애그리거트
이때 생각해볼 수 있는 방법은 '주문 애그리거트'가 필요한 데이터를 모두 가지도록 한 뒤 할인 금액 계산 책임을 주문 애그리거트에 할당하는 방법입니다. 만약 이때, 특별 감사 세일을 적용해 한 달간 2% 추가 할인을 적용하면 어떻게 될까요? 이 할인 정책은 주문 애그리거트의 구성요소와 관련이 없음에도 주문 애그리거트의 코드를 수정해야 합니다. 이 경우 다음과 같은 문제점이 발생합니다.
- 자신의 책임 범위를 넘어서는 기능을 구현하게 된다.
- 외부에 대한 의존이 높아지게 된다.
- 코드를 복잡하게 만들어 수정을 어렵게 만든다.
- 애그리거트의 범위를 넘어서는 도메인 개념이 숨어들어 명시적으로 드러나지 않게 된다.
따라서,
한 애그리거트에 넣기 애매한 도메인 기능을 억지로 특정 애그리거트에 구현하면 안됩니다.
이런 문제를 해소하는 가장 쉬운 방법이 도메인 기능을 별도 서비스로 구현하는 것입니다.
도메인 서비스
도메인 서비스는
도메인 영역에 위치한 도메인 로직을 표현할 때 사용합니다.
주로 다음 상황에 도메인 서비스를 사용합니다.
- 계산 로직
- 여러 애그리거트가 필요한 계산 로직이나, 한 애그리거트에 넣기에는 다소 복잡한 계산 로직
- 외부 시스템 연동이 필요한 도메인 로직
- 구현하기 위해 타 시스템을 사용해야 하는 도메인 로직
계산 로직과 도메인 서비스
위의 예시와 같은 경우 애그리거트에 억지로 넣기보다는 도메인 서비스를 이용해 도메인 개념을 명시적으로 드러내면 됩니다.
- 응용 영역의 서비스 → 응용 로직
- 도메인 서비스 → 도메인 로직
도메인 영역의 구성요소와 도메인 서비스의 차이점은
도메인 서비스는 상태 없이 로직만 구현한다는 점입니다.
- 도메인 서비스를 구현하는 데 필요한 상태는 다른 방법으로 전달받습니다.
할인 금액 계산을 예로 들어보겠습니다.
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(iscountCalculationService disCalSvc, MemberGrade grade {
Money totalAmounts = getTotalAmounts();
Money discountAmounts = disCalSvc.calculateDiscountAmounts(this.orderLines, this.coupons, grade);
this.paymentAmounts = totalAmounts.minus(discountAmounts);
}
}
애그리거트 객체에 도메인 서비스를 전달하는 것은 응용 서비스의 책임입니다.
public class OrderService {
private DiscountCalculationService discountCalculationService;
private Order createOrder(OrderNo orderNo, OrderRequest orderReq) {
// member와 order 조회
...
order.calculateAmounts(discountCalculationService, member.getGrade());
return order;
}
}
주의 사항
도메인 서비스 객체를 애그리거트에 주입하면 안됩니다.
- 도메인 객체 → 개념적으로 하나인 모델을 표현
- 모델의 데이터를 담는 필드 → 모델에서 중요한 구성요소
- 하지만 도메인 서비스 필드는 데이터 자체와는 관련이 없습니다.
- 일부 기능을 위해 굳이 도메인 서비스 객체를 애그리거트에 의존 주입할 이유가 없습니다.
반대로, 도메인 서비스의 기능을 실행할 때 애그리거트를 전달하기도 합니다.
대표적으로 계좌 이체 기능을 보면, 계좌 이체는 두 애그리거트가 관여합니다.
public class TransferService {
public void transfer(Account fromAcc, Account toAcc, Money amounts) {
fromAcc.withdraw(amounts);
toAcc.credit(amounts);
}
...
}
- 응용 서비스는 두 Account 애그리거트를 구한 뒤, 해당 도메인 영역의
TransferService
를 이용해 계좌 이체 도메인 기능을 실행합니다.
응용 서비스? 도메인 서비스?
특정 기능이 응용 서비스인지 도메인 서비스인지 감을 잡기 어려울 때는 다음 두 가지를 검사해보면 됩니다.
- 애그리거트의 상태를 변경하는 기능인가
- 애그리거트의 상태 값을 계산하는 기능인가
위의 계좌 이체 로직과 결제 금액 로직은 각각 애그리거트를 변경하고 애그리거트의 값을 계산하는 도메인 로직입니다. 도메인 로직이면서 한 애그리거트에 넣기 적합하지 않으므로 이 두 로직은 도메인 서비스로 구현하게 됩니다.
외부 시스템 연동과 도메인 서비스
외부 시스템이나 타 도메인과의 연동 기능도 도메인 서비스가 될 수 있습니다.
예시로 설문 조사 시스템과 사용자 역할 관리 시스템이 분리되어 있다고 하겠습니다. 설문 조사 시스템은 설문 조사를 생성할 때 사용자가 생성 권한을 가지는지 확인하기 위해 역할 관리 시스템과 연동해야 합니다.
시스템 간 연동은 HTTP API 호출로 이루어질 수 있지만, 설문 조사 도메인 입장에선 "사용자가 설문 조사 생성 권한을 가졌는가?"를 확인하는 도메인 로직으로 볼 수 있습니다.
이 도메인 로직은 다음과 같은 도메인 서비스로 표현할 수 있습니다.
public interface SurveyPermissionChecker {
boolean hasUserCreationPermission(String userId);
}
여기서 주의할 점은
- 역할 관리 시스템과 연동한다는 관점 X
- 권한을 확인한다는 도메인 로직 관점 O
응용 서비스는 다음과 같이 생성 권한을 검사합니다.
public class CreateSurveyService {
private SurveyPermissionChecker permissionChecker;
public Long createSurvey(CreateSurveyRequest req) {
vaildate(req);
// 도메인 서비스를 이용해 외부 시스템 연동을 표현
if(!permissionChecker.hasUserCreationPermission(req.getRequestorId())) {
throw new NoPermissionException();
}
...
}
}
SurveyPermissionChecker
인터페이스 구현체 → 인프라스트럭처 영역에 위치
도메인 서비스의 패키지 위치
도메인 서비스의 위치는 다른 도메인 구성요소와 동일한 패키지에 위치합니다.
도메인 서비스의 개수가 많거나 엔티티, 밸류와 같은 다른 구성요소와 명시적으로 구분하고 싶다면 domain
패키지 하위에 다음과 같이 구분해도 괜찮습니다.
- domain.model
- domain.service
- domain.repository
만약, 도메인 서비스의 구현이
- 특정 구현 기술에 의존
- 외부 시스테의 API를 실행
하는 상황이라면, 도메인 서비스는 인터페이스로 추상화하고 실제 구현은 인프라스트럭처 영역에 위치해야 합니다. 이를 통해
- 도메인 영역이 특정 구현에 종속되는 것을 방지
- 도메인 영역에 대한 테스트 용이
라는 효과를 받을 수 있습니다.
'DDD' 카테고리의 다른 글
[DDD] : 표현 영역 (0) | 2025.03.11 |
---|---|
[DDD] : 응용 영역 (0) | 2025.03.10 |
[DDD] : 리포지터리와 모델 구현 (0) | 2025.03.06 |
[DDD] : 아키텍처 (0) | 2025.03.04 |
[DDD] : 도메인 모델 (0) | 2025.02.27 |