728x90
도메인 주도 개발 시작하기 7장을 요약한 내용입니다.
7.1 여러 애그리거트가 필요한 기능
- 한 애그리거트로 기능을 구현할 수 없을 때가 있다.
- 결제 금액을 계산할 때 필요한 내용
- 상품 애그리거트: 구매하는 상품의 가격이 필요하다. 상품에 따라 배송비가 추가되기도 한다.
- 주문 애그리거트: 상품별로 구매 개수가 필요하다.
- 할인 쿠폰 애그리거트: 쿠폰별로 지정한 할인 금액이나 비율에 따라 주문 총 금액을 할인한다. 할인 쿠폰을 조건에 따라 중복 사용할 수 있다거나 지정한 카테고리의 상품에만 적용할 수 있다는 제약 조건이 있다면 할인 계산이 복잡해지낟.
- 회원 애그리거트: 회원 등급에 따라 추가 할인이 가능하다.
- 결제 금액을 계산해야 하는 주체는 어떤 애그리거트일까?
- 총 주문 금액에서 할인 금액을 계산해야 하는데 이 할인 금액을 구하는 것은 누구 책임일까?
- 할인 쿠폰이 할인 규칙을 갖고 있으니 할인 쿠폰 애그리거트가 계산해야 할까?
- 주문 애그리거트가 필요한 데이터를 모두 가지도록 한 뒤 할인 금액 계산 책임을 주문 애그리거트에 할당
- 결제 금액 계산 로직이 주문 애그리거트의 책임이 맞을까?
- 할인 정책 추가로 인한 수정으로 주문 애그리거트의 코드 수정을 해야할 수도…
public class Order { private Orderer orderer; private List<OrderLine> orderLines; private List<Coupon> usedCoupons; private Money calculatePayAmounts() { Money totalAmounts = calculateTotalAmounts(); Money discount = coupons.stream() .map(coupon -> calculateDiscount(coupon)) .reduce(Money(0), (v1, v2) -> v1.add(v2)); Money membershipDiscount = calculateDiscount(orderer.getMember().getGrade()); return totalAmounts.minus(discount).minus(membershipDiscount); } private Money calculateDiscount(Coupon coupon) { // orderLines의 각 상품에 대해 쿠폰을 적용해서 할인 금액 계산하는 로직. // 쿠폰의 적용 조건 등을 확인하는 코드 // 정책에 따라 복잡한 if-else와 계산 코드 } private Money calculateDiscount(MemberGrade grade) { // 등급에 따라 할인 금액 계산 } }
- 한 애그리거트에 넣기 애매한 도메인 기능을 억지로 특정 애그리거트에 구현하면 안된다.
- 자신의 책임 범위를 넘어서는 기능을 구현하기 때문에 코드가 길어지고 외부에 대한 의존이 높아지게 됨
- 코드를 복잡하게 만들어 수정이 어렵다
- 도메인 기능을 별도 서비스로 구현하는 것이 좋다.
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); } private Money calculateDiscount(Coupon coupon) { // orderLines의 각 상품에 대해 쿠폰을 적용해서 할인 금액 계산하는 로직. // 쿠폰의 적용 조건 등을 확인하는 코드 // 정책에 따라 복잡한 if-else와 계산 코드 } private Money calculateDiscount(MemberGrade grade) { // 등급에 따라 할인 금액 계산 } }
- 할인 계산 서비스를 사용하는 주체는 애그리거트가 될 수도 있고 응용 서비스가 될 수도 있다.
- public class Order { public void cacluateAmounts(DiscountCalculationService disCalSvc, MemberGrade grade) { Money totalAmounts = getTotalAmounts(); Money discountAmounts = disCalSvc.calculateDiscountAmount(this.orderLines, this.coupons, grade); 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.getOrderLines(), orderReq.getCoupons(), createOrderer(member), orderReq.getShippingInfo()); order.calculateAmounts(this.discountCalculationService, member.getGrade()); return order; } }
- 애그리거트 메서드를 실행할 때 도메인 서비스를 인자로 전달하지 않고 도메인 서비스의 기능을 실행할 때 애그리거트를 전달하기도 한다.
- 계좌 이체에는 두 계좌 애그리거트가 관여하는데 한 애그리거트는 금액을 출금하고 한 애그리거트는 금액을 입금한다.
- 응용 서비스는 두 Account 애그리거트를 구한뒤 계좌 이체 도메인 기능을 실행
public class TransferService { public void transfer(Account fromAcc, Account toAcc, Money amounts) { fromAcc.withdraw(amounts); toAcc.credit(amounts) } }
- 도메인 서비스는 도메인 로직을 수행하지 응용 로직을 수행하진 않는다.
- 트랜잭션 처리와 같은 로직은 응용 로직이므로 응용 서비스에서 처리 해야한다.
<aside> 👉 특정 기능이 응용 서비스인지 도메인 서비스인지 감을 잡기 어려울 때는 해당 로직이 애그리거트의 상태를 변경하거나 애그리거트의 상태 값을 계산하는지 검사해 보면 된다.
</aside>
Note: 도메인 서비스 객체를 애그리거트에 주입하지 않기
- 애그리거트의 메서드를 실행할 때 도메인 서비스 객체를 파라미터로 전달한다는 것은 애그리거트가 도메인 서비스에 의존한다는 것을 의미
- public class Order { @Autowired private DiscountCalculationService discountCalculationService; ... }
- 도메인 객체는 필드로 구성된 데이터와 메서드를 이용해서 개념적으로 하나인 모델을 표현
- discountCalculationService 필드는 데이터 자체와는 관련이 없음
- Order 객체를 DB에 보관할 때 다른 필드와 달리 저장 대상이 아님
- Order가 제공하는 모든 기능에서 discountCalculationService를 필요로 하는 것은 아님
- 일부 기능만 필요로함
- 일부 기능을 위해 도메인 서비스 객체를 애그리거트에 의존 주입할 이유는 없음
7.2.2 외부 시스템 연동과 도메인 서비스
- 외부 시스템이나 타 도메인과의 연동 기능도 도메인 서비스가 될수 있다.
- 설문 조사 시스템과 사용자 역활 관리 시스템이 분리되어 있을 때
- 설문 조사 시스템은 설문 조사를 생성할 때 사용자가 생성 권한을 확인하기 위해 역활 관리 시스템과 연동 해야함
- 시스템 간 연동은 HTTP API 호출로 이루어질 수 있지만 설문 조사 도메인 입장에서는 사용자가 설문 조사 생성 권한을 가졌는지 확인하는 도메인 로직으로 볼수 있다.
- 도메인 로직 관점에서 역할 관리 시스템과 연동하는 관점으로 인터페이스를 작성
public interface SurveyPermissionChecker { boolean hasUserCreationPermission(String userId); }
- 응용 서비스는 도메인 서비스를 이용해서 생성 권한을 검사
public class CreateSurveyService { private ServeyPermissionChecker permissionChecker; public Long createSurvey(CreateSurveyRequest req) { validate(req); if (!permissionChecker.hasUserCreationPermission(req.getRequestorId()) { throw new NoPermissionException(); } } }
7.2.3 도메인 서비스의 패키지 위치
- 도메인 서비스는 도메인 로직을 표현하므로 도메인 서비스의 위치는 다른 도메인 구성요소와 동일한 패키지에 위치
- 도메인 서비스의 개수가 많거나 엔티티나 밸류와 같은 다른 구성요소와 명시적으로 구분하고 싶다면 domain 패키지 밑에 하위 패키지를 구분하여 위치시켜도 된다.
- domain.model
- domain.service
- domain.repository
7.2.4 도메인 서비스의 인터페이스와 클래스
- 도메인 서비스의 로직이 고정되어 있지 않은 경우 도메인 서비스 자체를 인터페이스 구현하고 이를 구현한 클래스를 둘 수도 있다.
- 도메인 로직을 외부 시스템이나 별도 엔진을 이용해서 구현할 때 인터페이스와 클래스를 분리하게 된다.
- 도메인 영역에는 도메인 서비스 인터페이스가 위치하고 실제 구현은 인프라스트럭처 영역에 위치
- 도메인 서비스의 구현이 특정 구현 기술에 의존하거나 외부 시스템의 API를 실행한다면 도메인 영역의 도메인 서비스는 인터페이스로 추상화해야 한다.
- 도메인 영역이 특정 구현에 종속되는 것을 방지
- 도메인 영역에 대한 테스트가 쉬워짐
7.1 여러 애그리거트가 필요한 기능
- 한 애그리거트로 기능을 구현할 수 없을 때가 있다.
- 결제 금액을 계산할 때 필요한 내용
- 상품 애그리거트: 구매하는 상품의 가격이 필요하다. 상품에 따라 배송비가 추가되기도 한다.
- 주문 애그리거트: 상품별로 구매 개수가 필요하다.
- 할인 쿠폰 애그리거트: 쿠폰별로 지정한 할인 금액이나 비율에 따라 주문 총 금액을 할인한다. 할인 쿠폰을 조건에 따라 중복 사용할 수 있다거나 지정한 카테고리의 상품에만 적용할 수 있다는 제약 조건이 있다면 할인 계산이 복잡해지낟.
- 회원 애그리거트: 회원 등급에 따라 추가 할인이 가능하다.
- 결제 금액을 계산해야 하는 주체는 어떤 애그리거트일까?
- 총 주문 금액에서 할인 금액을 계산해야 하는데 이 할인 금액을 구하는 것은 누구 책임일까?
- 할인 쿠폰이 할인 규칙을 갖고 있으니 할인 쿠폰 애그리거트가 계산해야 할까?
- 주문 애그리거트가 필요한 데이터를 모두 가지도록 한 뒤 할인 금액 계산 책임을 주문 애그리거트에 할당
- 결제 금액 계산 로직이 주문 애그리거트의 책임이 맞을까?
- 할인 정책 추가로 인한 수정으로 주문 애그리거트의 코드 수정을 해야할 수도…
public class Order { private Orderer orderer; private List<OrderLine> orderLines; private List<Coupon> usedCoupons; private Money calculatePayAmounts() { Money totalAmounts = calculateTotalAmounts(); Money discount = coupons.stream() .map(coupon -> calculateDiscount(coupon)) .reduce(Money(0), (v1, v2) -> v1.add(v2)); Money membershipDiscount = calculateDiscount(orderer.getMember().getGrade()); return totalAmounts.minus(discount).minus(membershipDiscount); } private Money calculateDiscount(Coupon coupon) { // orderLines의 각 상품에 대해 쿠폰을 적용해서 할인 금액 계산하는 로직. // 쿠폰의 적용 조건 등을 확인하는 코드 // 정책에 따라 복잡한 if-else와 계산 코드 } private Money calculateDiscount(MemberGrade grade) { // 등급에 따라 할인 금액 계산 } }
- 한 애그리거트에 넣기 애매한 도메인 기능을 억지로 특정 애그리거트에 구현하면 안된다.
- 자신의 책임 범위를 넘어서는 기능을 구현하기 때문에 코드가 길어지고 외부에 대한 의존이 높아지게 됨
- 코드를 복잡하게 만들어 수정이 어렵다
- 도메인 기능을 별도 서비스로 구현하는 것이 좋다.
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); } private Money calculateDiscount(Coupon coupon) { // orderLines의 각 상품에 대해 쿠폰을 적용해서 할인 금액 계산하는 로직. // 쿠폰의 적용 조건 등을 확인하는 코드 // 정책에 따라 복잡한 if-else와 계산 코드 } private Money calculateDiscount(MemberGrade grade) { // 등급에 따라 할인 금액 계산 } }
- 할인 계산 서비스를 사용하는 주체는 애그리거트가 될 수도 있고 응용 서비스가 될 수도 있다.
- public class Order { public void cacluateAmounts(DiscountCalculationService disCalSvc, MemberGrade grade) { Money totalAmounts = getTotalAmounts(); Money discountAmounts = disCalSvc.calculateDiscountAmount(this.orderLines, this.coupons, grade); 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.getOrderLines(), orderReq.getCoupons(), createOrderer(member), orderReq.getShippingInfo()); order.calculateAmounts(this.discountCalculationService, member.getGrade()); return order; } }
- 애그리거트 메서드를 실행할 때 도메인 서비스를 인자로 전달하지 않고 도메인 서비스의 기능을 실행할 때 애그리거트를 전달하기도 한다.
- 계좌 이체에는 두 계좌 애그리거트가 관여하는데 한 애그리거트는 금액을 출금하고 한 애그리거트는 금액을 입금한다.
- 응용 서비스는 두 Account 애그리거트를 구한뒤 계좌 이체 도메인 기능을 실행
public class TransferService { public void transfer(Account fromAcc, Account toAcc, Money amounts) { fromAcc.withdraw(amounts); toAcc.credit(amounts) } }
- 도메인 서비스는 도메인 로직을 수행하지 응용 로직을 수행하진 않는다.
- 트랜잭션 처리와 같은 로직은 응용 로직이므로 응용 서비스에서 처리 해야한다.
<aside> 👉 특정 기능이 응용 서비스인지 도메인 서비스인지 감을 잡기 어려울 때는 해당 로직이 애그리거트의 상태를 변경하거나 애그리거트의 상태 값을 계산하는지 검사해 보면 된다.
</aside>
Note: 도메인 서비스 객체를 애그리거트에 주입하지 않기
- 애그리거트의 메서드를 실행할 때 도메인 서비스 객체를 파라미터로 전달한다는 것은 애그리거트가 도메인 서비스에 의존한다는 것을 의미
- public class Order { @Autowired private DiscountCalculationService discountCalculationService; ... }
- 도메인 객체는 필드로 구성된 데이터와 메서드를 이용해서 개념적으로 하나인 모델을 표현
- discountCalculationService 필드는 데이터 자체와는 관련이 없음
- Order 객체를 DB에 보관할 때 다른 필드와 달리 저장 대상이 아님
- Order가 제공하는 모든 기능에서 discountCalculationService를 필요로 하는 것은 아님
- 일부 기능만 필요로함
- 일부 기능을 위해 도메인 서비스 객체를 애그리거트에 의존 주입할 이유는 없음
7.2.2 외부 시스템 연동과 도메인 서비스
- 외부 시스템이나 타 도메인과의 연동 기능도 도메인 서비스가 될수 있다.
- 설문 조사 시스템과 사용자 역활 관리 시스템이 분리되어 있을 때
- 설문 조사 시스템은 설문 조사를 생성할 때 사용자가 생성 권한을 확인하기 위해 역활 관리 시스템과 연동 해야함
- 시스템 간 연동은 HTTP API 호출로 이루어질 수 있지만 설문 조사 도메인 입장에서는 사용자가 설문 조사 생성 권한을 가졌는지 확인하는 도메인 로직으로 볼수 있다.
- 도메인 로직 관점에서 역할 관리 시스템과 연동하는 관점으로 인터페이스를 작성
public interface SurveyPermissionChecker { boolean hasUserCreationPermission(String userId); }
- 응용 서비스는 도메인 서비스를 이용해서 생성 권한을 검사
public class CreateSurveyService { private ServeyPermissionChecker permissionChecker; public Long createSurvey(CreateSurveyRequest req) { validate(req); if (!permissionChecker.hasUserCreationPermission(req.getRequestorId()) { throw new NoPermissionException(); } } }
7.2.3 도메인 서비스의 패키지 위치
- 도메인 서비스는 도메인 로직을 표현하므로 도메인 서비스의 위치는 다른 도메인 구성요소와 동일한 패키지에 위치
- 도메인 서비스의 개수가 많거나 엔티티나 밸류와 같은 다른 구성요소와 명시적으로 구분하고 싶다면 domain 패키지 밑에 하위 패키지를 구분하여 위치시켜도 된다.
- domain.model
- domain.service
- domain.repository
7.2.4 도메인 서비스의 인터페이스와 클래스
- 도메인 서비스의 로직이 고정되어 있지 않은 경우 도메인 서비스 자체를 인터페이스 구현하고 이를 구현한 클래스를 둘 수도 있다.
- 도메인 로직을 외부 시스템이나 별도 엔진을 이용해서 구현할 때 인터페이스와 클래스를 분리하게 된다.
- 도메인 영역에는 도메인 서비스 인터페이스가 위치하고 실제 구현은 인프라스트럭처 영역에 위치
- 도메인 서비스의 구현이 특정 구현 기술에 의존하거나 외부 시스템의 API를 실행한다면 도메인 영역의 도메인 서비스는 인터페이스로 추상화해야 한다.
- 도메인 영역이 특정 구현에 종속되는 것을 방지
- 도메인 영역에 대한 테스트가 쉬워짐
728x90
'도메인 주도 개발 스터디' 카테고리의 다른 글
Chapter 9 도메인 모델과 바운디드 컨텍스트 (0) | 2023.11.08 |
---|---|
Chapter 8 애그리거트 트랜잭션 관리 (0) | 2023.11.08 |
Chapter 6 응용 서비스와 표현 영역 (0) | 2023.11.08 |
Chapter 5 스프링 데이터 JPA를 이용한 조회 기능 (0) | 2023.11.08 |
Chapter 4 리포지터리와 모델 구현 (1) | 2023.11.08 |