Aggreagte는 관련된 객체를 하나의 군으로 묶어준다.


Aggregate와 그에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다.Aggregate는 위 그림에서 보는것 처럼 경계를 갖는다.Aggregate에 속한 객체는 다른 Aggregate에 속하지 않는다.Aggregate는 독립된 군이며, 오로지 자기 자신을 관리할 뿐, 다른 Aggregate는 관리하지 않는다.Aggregate에 속한 모든 객체가 일관된 상태를 유지하려면 Aggregate 전체를 관리할 주체가 필요하다.Aggregate Root이다.Aggregate에 속한 객체는 Aggregate Root에 직접 또는 간접적으로 속하게 된다.
Aggregate의 일관성이 깨지지 않아야 한다.Aggregate가 제공해야 할 도메인 기능을 구현한다.public class Order {
// Aggregate Root는 도메인 규칙을 구현한 기능을 제공한다.
public void changeShippingInfo(ShippingInfo newShippingInfo) {
verifyNotYetShipped();
setShippingInfo(newShippingInfo);
}
private void verifyNotYetShipped() {
if (state != OrderState.PAYMENT_WAITING && state != OrderState.PREPARING) {
throw new IllegalStateException("already shipped");
}
}
}
단순히 필드를 변경하는 set 메서드를 public 범위로 만들지 않는다.
Value타입은 불변으로 구현한다.Value타입 데이터를 변경할 때는 객체 자체를 완전히 교체한다.
- Aggregate 외부에서 Aggregate에 속한 객체를 직접 변경하면 안 된다.
- 데이터 일관성이 꺠지게 된다.
ShippingInfo shippingInfo = order.getShippingInfo();
shippingInfo.setAddress(newAddress);
public class Order {
private Money totalAmounts;
private List<OrderLine> orderLines;
private void calculateTotalAmounts() {
int sum = orderLines.stream()
.mapToInt(orderLine -> orderLine.getPrice() * orderLine.getQuantity())
.sum();
this.totalAmounts = new Money(sum);
}
}
public class OrderLines {
private List<OrderLine> lines;
public Money getTotalAmounts() {
// ...
}
// public일 경우, 외부에서 OrderLines의 상태를 변경할 수 있다.
// 오로지 Aggregate Root에서만 변경할 수 있게끔, package 혹은 protected로 설정한다.
protected void changeOrderLines(List<OrderLine> newLines) {
this.lines = newLines;
}
}
public class Order {
private Money totalAmounts;
private OrderLines orderLines;
public void changeOrderLines(List<OrderLine> newLines) {
orderLines.changeOrderLines(newLines);
this.totalAmounts = orderLines.calculateTotalAmounts();
}
}
Application 영역에서 여러 Aggregate를 수정하도록 구현하는 걸 권장한다.Repository는 Aggregate 단위로 존재한다.Repository는 기본적으로 Aggregate를 저장하고 조회하는 기능을 제공한다.Repository는 Aggregate 전체를 저장소에 영속화해야한다.

한 Aggregate가 관리하는 범위는 자기 자신으로 한정해야 한다.
public class Order {
private Orderer orderer;
public void changeShippingInfo(ShippingInfo newShippingInfo, boolean useNewShippingAddAsMemberAddress) {
if (useNewShippingAddAsMemberAddress) {
orderer.getMember().changeAddress(newShippingInfo); // Not Good...
}
}
}

Member member = memberRepository.findById(ordererId);
List<Order> orders = orderRepository.findByOrderer(ordererId);
List<OrderView> orderViews = orders.stream()
map(order -> {
ProductId productId = order.getOrderLines().get(0).getProductId();
// 각 주문마다 첫 번쨰 주문 상품 정보 로딩을 위한 쿼리 실행 (N+1 쿼리 현상과 비슷)
Product product = productRepository.findById(productId);
return new OrderView(order, member, product);
}).toList();
@Repository
public class JpaOrderViewDao implements OrderViewDao {
@Override
public List<OrderView> selectByOrderer(String ordererId) {
// Order, Member, Product 조인 쿼리 수행
// Result 변환
}
}
public class RegisterProductService {
public ProductId registerNewProduct(NewProductRequest req) {
Store store = storeRepository.findById(req.getStoreId());
checkNull(store);
// Store가 Blocked 상태인지 확인
if (store.isBlocked()) {
throw new StoreBlockedException();
}
// Store가 신규 Product 생성
ProductId productId = productRepository.nextId();
Product product = new Producct(productId, store.getId(), ...);
productRepository.save(product);
return product.getId();
}
}
public class Store {
public Product createProduct(ProductId productId, ProductInfo productInfo) {
if (isBlocked()) {
throw new StoreBlockedException();
}
// return new Product(productId, this.id, productInfo);
return ProductFactory.create(productId, this.id, productInfo);
}
}
public class RegisterProductService {
public ProductId registerNewProduct(NewProductRequest req) {
Store store = storeRepository.findById(req.getStoreId());
checkNull(store);
ProductId productId = productRepository.nextId();
Product product = store.createProduct(productId, ...);
productRepository.save(product);
return product.getId();
}
}