Order
의 totalAmounts
값은 OrderLine
의 quantity
값이 변경되면 함께 바뀌어야 한다.set
메서드를 public
으로 구현하지 않아야 한다.애그리거트 루트는 애그리거트 내부의 다른 객체를 조합해서 기능을 완성한다.
// Order는 총 주문 금액을 구하기 위해 OrderLine 목록을 사용한다.
public class Order {
private Money totalAmounts;
private List<OrderLine> orderLines;
private void calculateTotalAmounts() {
int sum = orderLines.stream()
.mapToInt(ol -> ol.getPrice() * ol.getQuantity())
.sum();
this.totalAmounts = new Money(sum);
}
애그리거트 루트가 구성 요소의 상태를 참조하는 것 뿐만 아니라 기능 실행을 위임하기도 한다.
// OrderLine 목록을 가지는 일급 컬렉션
public class OrderLines {
private List<OrderLine> lines;
public Money getTotalAmounts() { ... }
public void changeOrderLines(List<OrderLine> newLines) {
this.lines = newLines;
}
}
public class Order {
private OrderLines orderLines;
private Money totalAmounts;
// 일급 컬렉션 OrderLines에 기능 실행을 위임
public void changeOrderLines(List<OrderLine> newLines) {
orderLines.changeOrderLines(newLines);
this.totalAmounts = orderLines.getTotalAmounts();
}
}
Order
에서 getOrderLines()
메서드를 통해 OrderLines
를 반환한 뒤, Order
객체 외부에서에서 OrderLines.changeOrderLines()
를 호출하면 업무 규칙 일관성이 깨지게 된다.OrderLines
를 변경할 수 없도록 불변으로 구현해야 한다. (불변 컬렉션 사용) // 응용 서비스에서 각 애그리거트를 변경하는 코드
public ChangeOrderService{
@Transactional
public void changeShippingInfo(OrderId id, ShippingInfo newShippingInfo, boolean useNewShippingAddrAsMemberAddr) {
Order order = findOrderById(id);
order.shipTo(newShippingInfo);
if useNewShippingAsMemberAddr) {
Member member = findMember(order.getOrderer());
member.changeAddress(newShippingInfo.getAddress());
}
}
}
Order
와 OrderLine
을 별도의 DB 테이블에 저장한다고 해서 각각의 리포지터리를 만들지는 않는다.save()
findById()
Order
에그리거트를 조회하면 내부에 OrderLine
, Orderer
등 모든 요소를 포함해야 한다.애그리거트 간 참조는 필드를 통해 쉽게 구현할 수 있다.
public class Order {
private Member orderer;
// ...
}
ID를 이용해서 다른 애그리거트를 참조하면 위 세 가지 문제를 완화시킬 수 있다.
public class Order {
private MemberId memberId;
}
1-N(일대다) 연관에서는 1이 N을 갖는 것이 아닌 N이 1을 참조하는 식으로 구현해야 한다.
public class Product {
// ...
private CategoryId categoryId;
// ...
애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면 애그리거트에 팩토리 메서드를 구현하는 것을 고려해 보자.
‘신고 당해 차단된 상점은 물건을 등록하지 못한다’ 기능을 팩토리 없이 구현
public class RegisteredProductService {
public ProductId registerNewProduct(NewProductRequest req) {
Store store = findStoreById(req.getStoreId());
if (store.isBlocked()) {
throw new StoreBlockedException();
}
Product product = new Product(store.getId(), req.getName());
return new ProductId(productRepository.save(product));
}
}
Product
생성 코드와 Product
를 생성 가능한지 판단하는 코드가 분리되어 있다.Store
애그리거트에 Product
생성 기능을 위임
public class Store {
public Product createProduct(ProductId newProductId, String name) {
if (isBlocked()) {
throw new StoreBlockedException();
}
return new Product(this.id, name);
}
public class RegisteredProductService {
public ProductId registerNewProduct(NewProductRequest req) {
Store store = findStoreById(req.getStoreId());
Product product = store.createProduct(req.getName());
return new ProductId(productRepository.save(product));
}
}
Store.createProduct()
는 상품 애그리거트를 생성하는 팩토리 역할을 한다.Product
생성 가능 여부 로직을 변경해도 도메인 영역의 Store
만 변경하면 되고 응용 서비스는 영향 받지 않는다.Store
애그리거트가 Product
를 생성할 때 많은 정보를 알아야 한다면 다른 팩토리에 위임하는 방법도 있다.
public class Store {
public Product createProduct(ProductInfo info) {
if (isBlocked()) {
throw new StoreBlockedException();
}
return ProductFactory.create(this.id, info);
}
}