효습
7장 도메인 서비스 본문
7.1 여러 애그리거트가 필요한 기능
도메인 영역의 코드를 작성하다보면, 한 애그리거트로 기능을 구현할 수 없을 때가 있다.
결제 계산 로직을 예로 들면,
- 상품 애그리거트 : 구매하는 상품의 가격과 배송비 관련 정보가 필요함
- 주문 애그리거트 : 상품의 구매 개수 정보가 필요
- 할인 쿠폰 애그리거트: 할인 쿠폰의 적용 여부 , 할인 쿠폰 사용에 관련한 제약 여부
- 회원 : 회원에 따른 할인 여부
이 상황에서 결제 금액을 계산하는 애그리거트는 어떤 애그리거트일까?
→ 생각해볼 수 있는 방법은 주문 애그리거트에 필요한 데이터를 모두 가지도록 한 뒤, 할인 금액 계산 책임을 주문 애그리거트에 할당하는 것이다.
그렇다면 결제 금액 계산 로직이 주문 애그리거트의 책임이 맞느냐?
특별 행사로 한 달간 2% 추가 할인이 들어간다고 생각해보자. 이 할인 정책은 주문 애그리거트가 가지고 있는 구성 요소와는 아무런 관계가 없으나 주문 애그리거트가 결제 계산 책임을 가지고 있다는 이유로 주문 애그리거트 코드를 수정해야한다.
→ 한 애그리거트에 모든 책임을 지게 하는 건 적합하지 않음. 자신의 책임을 벗어나는 기능을 구현하게 되고 외부에 대한 의존도도 높아지게 된다. 코드가 복잡해짐에 따라 수정이 어렵게 됨
무엇보다도 애그리거트의 범위를 넘어서는 도메인 개념이 명시적으로 들어나지 않게 됨
→ 해결책은 도인 기능을 별도 서비스로 구현하는 것이다.
7.2 도메인 서비스
도메인 서비스는 도메인 영역에 위치한 도메인 로직을 표현할 때 사용한다.
- 계산 로직 : 여러 애그리거트가 필요한 계산 로직이나, 한 애그리거트에 넣기에는 다소 복잡한 계산 로직
- 외부 시스템 연동이 필요한 도메인 로직 : 구현하기 위해 타 시스템을 사용해야 하는 도메인 로직
7.2.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, grade);
this.paymentAmounts = totalAmounts.minus(discountAmounts);
}
}
도메인 서비스 객체를 애그리거트에 주입하지 않기
- 애그리거트의 메서드를 실행할 때 도메인 서비스 객체를 파라미터로 전달한다는 것은 애그리거트가 도메인 서비스에 의존한다는 것을 의미한다.
- 이때 애그리거트가 의존하는 도메인 서비스를 의존 주입으로 처리하고 싶을 수 있으나 필자는 좋은 방법은 아니라고 생각한다.
- 예를 들어 Order 애그리거트에 할인 계산 도메인 서비스를
@Autowired
를 사용하여 의존주입하였다고 해보자- 도메인 객체는 필드(프로퍼티)로 구성된 데이터와 메서드를 이용해서 개념적으로 하나의 모델을 표현한다.
- 모델의 데이터를 담는 필드는 모델에서 중요한 구성요소다.
- 근데 여기서 할인 계산 도메인 서비스인
DiscountCalculationService
필드는 데이터 자체와 아무런 관련이 없으며 DB 저장 대상도 아니다. - 또 Order이 제공하는 모든 기능에서
DiscountCalculationService
이 필요하지도 않고 일부 기능에서만 필요함 - 굳이 의존 주입할 필요가 없음
도메인 서비스의 기능을 실행할 때 애그리거트를 전달하기도 한다.
예를 들면 계좌 이체 기능이다.
//계좌 이체 도메인 서비스 - 기능을 실행할 때 애그리거트 전달
public class TransferService {
public void transfer(Account fromAcc, Account toAcc, Money amounts) {
fromAcc.withdraw(amounts);
toAcc.credit(amounts);
}
}
- 한 번의 계좌 이체에 두 계좌 애그리거트가 관여하는데 한 애그리거트는 금액을 출금하고 한 애그리거트는 금액을 입금한다.
- 응용 서비스는 두 애그리거트를 구한 뒤 도메인 영역의 서비스를 이용해서 계좌 이체 기능을 구현한다.
도메인 서비스는 도메인 로직을 수행하지 응용 로직을 수행하지는 않는다.
트랜잭션 처리와 가은 로직은 응용 로직이므로 도메인 서비스가 아닌 응용 서비스에서 처리해야한다.
특정 기능의 로직이 애그리거트의 상태를 변경한다면 도메인 서비스로 구현하는 게 적합하다.
7.2.2 외부 시스템 연동과 도메인 서비스
외부 시스템이나 타 도메인과의 연동 기능도 도메인 서비스가 될 수 있음
예를 들어 설문 조사 시스템에서 사용자가 설문 조사를 생성할 수 있는 권한을 가진 사용자인지 확인하기 위해 역할 관리 시스템과 연동해야하는 상황이라고 가정하자
- 시스템 간 연동은 api 호출로 이루어질 수 있다
- 하지만 설문 조사 도메인 입장에서는 확인하는 로직을 도메인 로직으로 볼 수 있으므로 도메인 서비스로 구현할 수 있다.
- 도메인 로직 관점에서 인터페이스로 작성한 것이다. 다른 시스템과 연동한다는 관점에서 인터페이스로 작성한 것이 아닌
// 사용자 확인 서비스 인터페이스
public interface SurveyPermissionChecker{
boolean hasUserCreationPermission(String userId);
}
- 응용 서비스는 이 도메인 서비스를 이용해서 생성 권한을 검사한다.
- 위의 인터페이스로 구현한 클래스는 인프라스트럭쳐 영역에 위치하여 연동을 포함한 권한 검사 기능을 구현한다.
7.2.3 도메인 서비스의 패키지 위치
도메인 서비스는 도메인 로직을 표현하므로 도메인 서비스의 위치는 다른 도메인 구성요소와 동일한 패키지에 위치한다.
7.2.4 도메인 서비스의 인터페이스와 클래스
- 도메인 서비스의 로직이 고정되어 잇지 않은 경우 도메인 서비스 자체를 인터페이스로 구현하고 이를 구현한 클래스를 이용할 수 있다.
- 특히 도메인 로직을 외부 시스템이나 별도 엔진을 이용해서 구현할 때 인터페이스와 클래스를 분리하게 된다.
- 도메인 서비스의 구현이 특정 구현 기술에 의존하거나 외부 시스템의 API를 실행한다면 도메인 영역의 도메인 서비스는 인터페이스로 추상화해야 한다.
- 이를 통해 도메인 영역이 특정 구현에 종속되는 것을 방지할 수 있고 도메인 영역에 대한 테스트가 쉬워진다.
'책 > 도메인 주도 개발 시작하기' 카테고리의 다른 글
9장 도메인 모델과 바운디드 컨텍스트 (2) | 2024.04.08 |
---|---|
8장 애그리거트 트랜잭션 관리 (1) | 2024.04.01 |
6장 응용 서비스와 표현 영역 (0) | 2024.03.25 |
5장 스프링 데이터 JPA를 이용한 조회 기능 (1) | 2024.03.25 |
4장 리포지터리 모델 구현 (2) | 2024.03.18 |