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();
}
}