[DDD START] 3장 애그리거트
22 Jun 2019애그리거트
- 애그리커트는 복잡한 모델을 관리하는 기준을 제공한다
도메인 규칙과 일관성
- set 메서드를 public 범위로 만들지 않는다
- 의미가 드러나는 메서드를 사용해서 구현
- 밸류 타입은 불변으로 한다
- 밸류 객체의 값을 변경하는 방법은 새로운 밸류 객체를 할당하는 것
애그리거트 루트
애그리거트의 일관성을 유지하기 위한 관리 주체
- 예)주문 애그리거트는 배송지 변경, 상품 변경과 같은 기능을 제공하는데 애그리거트 루트인 Order가 이 기능을 구현한 메서드를 제공한다
public class Order {
private ShippingInfo shippingInfo;
// 애그리거트 루트는 도메인 규칙을 구현한 기능을 제공한다
public void changeShippingInfo(ShippingInfo newShippingInfo) {
verifyNotYetShipped();
setShippingInfo(newShippingInfo);
}
private void verifyNotYetShipped() {
if (state != OrderState.PAYMENT_WAITING && state != OrderState.WAITING)
throw new IllegalStateException("already shipped");
}
// set 메서드의 접근 허용 범위는 private이다
// value를 불변으로 만들어 변경 시 새로운 객체를 할당하게 한다
private void setShippingInfo(ShippingInfo newShippingInfo) {
this.shippingInfo = newShippingInfo;
}
}
애그리거트 루트의 기능 구현
애그리거트 루트는 애그리거트 내부의 다른 객체를 조합해서 기능을 완성한다
- Order의 총 주문 금액 구하기
public class Order {
private Money totalAmounts;
private List<OrderLine> orderLines;
private void calculateTotalAmounts() {
int sum = orderLines.stream()
.mapToInt(ol -> ol.getPrice() * ol.quantity())
.sum();
}
}
애그리거트 루트는 내부의 다른 객체에 기능을 위임한다
- OrderLines를 별도의 클래스로 구현했다고 가정
public class Order {
private OrderLines orderLines;
public void changeOrderLines(List<OrderLine> newLines) {
orderLines.changeOrderLines(newLines);
this.totalAmounts = orderLines.getTotalAmounts();
}
}
트랜잭션 범위
- 트랜잭션 범위는 작을수록 좋다
- 애그리거트는 최대한 독립적이어야 한다
- 결합도를 낮춰야 한다
- 한 트랜잭션에서 두 개 이상의 애그리거트를 수정해야 한다면
- 애그리거트(Order Entity)에서 직접 수정하지 말고
- 응용서비스(ChangeOrderService)에서 두 애그리거트를 수정한다
- 도메인 이벤트(10장)를 사용하면 다른 애그리거트의 상태를 변경 가능
- 다음의 경우 한 트랜잭션에서 두 개 이상의 애그리거트를 변경하는 것을 고려
- 조직의 표준
- 조직의 표준에 따라 사용자 유스케이스와 관련된 응용 서비스의 기능을 한 트랜잭션으로 실행해야 하는 경우
- 기술 제약
- 기술적으로 이벤트 방식을 도입할 수 없는 경우
- UI 구현의 편리
- 주문 목록에서 여러 주문의 상태를 한 번에 변경하고 싶을 때 여러 주문 애그리거트의 상태를 한 트랜잭션에서 변경
- 조직의 표준
리포지터리와 애그리거트
리포지터리는 애그리거트 단위로 존재
- 애그리거트는 개념상 완전한 한 개의 도메인 모델을 표현
- 리포지토리는 애그리거트 단위로 존재한다
- OrderLine이 물리적으로 별도의 테이블에 저장된다고 해서 OrderLine을 위한 리포지토리를 별도로 만들지 않는다
애그리거트는 개념적으로 하나
- 리포지토리는 애그리거트 전체를 저장소에 영속화해야 한다
- Order 애그리거트를 저장할때 애그리거트 루트에 맵핑되는 테이블뿐만 아니라 애그리거트에 속하는 모든 구성요스를 위한 테이블에 데이터를 저장해야 한다
- 애그리거트를 구하는 리포지터리 메서드는 완전한 애그리거트를 제공해야 한다
- Order를 구하면 Order 애그리거트는 OrderLine, Orderer 등 모든 구성요소를 포함하고 있어야 한다
애그리거트 간 참조
필드를 통해 참조
public class orderer {
private Member member;
private String name;
}
order.getOrderer().getMember().getId()
- 구현의 편리함
- JPA의 경우 @ManyToOne, @OneToOne을 통해 쉽게 참조 가능
- 문제점
- 편한 탐색 오용
orderer.getCustomer().changeAddress()
- 성능에 대한 고민
- JPA의 경우 어떤 로딩 방식을 사용할 것인가?
- Eager 로딩
- 단순히 연관된 객체의 데이터를 함께 화면에 보여주어야 하면 유리
- Lazy 로딩
- 애그리거트의 상태를 변경할 경우 불필요한 객체를 함께 로딩할 필요가 없으므로 지연로딩이 유리
- 확장 어려움
- 도메인마다 다른 데이터 저장소를 사용할 경우 JPA같은 단일 기술을 사용할 수 없음
ID를 이용해서 다른 애그리거트를 참조
public class Orderer {
private MemberId memberId;
}
// ID를 이용해서 참조하는 애그리거트를 구한다
Customer customer = customerRepository.findById(
order.getOrderer().getCustomerId()
);
ID를 이용한 참조와 조회 성능
- 주문 목록을 보여주려면 상품 애그리거트와 회원 애그리거트를 함께 읽어야 해서 N+1 조회 문제가 발생한다
조회성능을 높이려면 전용 조회 쿼리를 사용해야 한다
@Repository
public class JpaOrderViewDao implements OrderViewDao {
@Override
public List<OrderView> selectByOrderer(String ordererId) {
String selectQuery =
"select .....
join ....";
}
}
애그리거트를 팩토리로 사용하기
애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면 애그리거트에 팩토리 메서드를 구현하는것을 고려한다
예)온라인 쇼핑몰에서 고객이 신고를 해서 특정 상점이 더 이상 물건을 등록하지 못하도록 차단한 상태를 구현
public class RegisterProductService {
public ProductId registerNewProduct(NewProductRequest req) {
Store account = accountRepository.findStoreById(req.getStoreId());
checkNull(account);
if (account.isBlocked()) {
throw new StoreBlockedException();
}
ProductId id = productRepository.nextId();
Product product = new Product(id, account.getId(), ...);
productRepository.save(product);
return id;
}
}
위 코드는 Product를 생성 가능한지 판단하는 코드와 Product를 생성하는 코드가 분리되어 있어서 나빠 보이지 않지만 중요한 도메인 로직 처리가 응용서비스에 노출되었다
이 도메인 기능을 Store 애그리거트에서 팩토리 메서드로 구현한다
public class Store extends Member {
public Product createProduct(ProductId newProductId, ...) {
if (isBlocked()) throw new StoreBlockedException();
return new Product(newProdcutId, getId, ...)
}
}
public class RegisterProductService {
public ProductId registerNewProduct(NewProductRequest req) {
Store account = accountRepository.findStoreById(req.getStoreId());
checkNull(account);
ProductId id = productRepository.nextId();
Product product = account.createProduct(id, ...);
productRepository.save(product);
return id;
}
}