애그리거트
- 도메인 객체 모델이 복잡해지면 개별 구성요소 위주로 모델을 이해하게 되고 전반적인 구조나 큰 수준에서 도메인 간의 관계를 파악하기 어려워진다.
→ 코드를 변경하고 확장하는 것이 어려워진다.
해결 방법
- 관련된 객체를 하나의 군으로 묶어 준다.
- 수많은 애그리거트로 묶어서 바라보면 상위 수준에서 도메인 모델 간의 관계를 파악할 수 있다.
특징
- 애그리거트 단위로 일관성을 관리하기 때문에, 애그리거트는 복잡한 도메인을 단산한 구조로 만들어준다. → 도메인 기능을 확장하고 변경하는 데 필요한 노력도 줄어든다.
- 한 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다.
- 보통 다수의 애그리거트가 한 개의 엔티티 객체만 갖는 경우만 많았으며 두 개 이상의 엔티티로 구성되는 애그리거트는 드물었음.
애그리거트 루트
- 애그리거트에 속한 모든 객체가 일관된 상태를 유지하려면 애그리거트 전체를 관리할 주체가 필요함
- 애그리거트 루트를 통해서만 도메인 로직을 구현하게 만들려면 도메인 모델에 대한 적용점
- 단순히 필드를 변경하는 set 메서드를 공개(public) 범위로 만들지 않는다.
- 밸류 타입은 불변으로 구현한다.
- 불변으로 구현할 수 없다면 접근 제한자 범위를 통해 애그리게이트 외부에서 상태 변경 기능을 실행하는 것을 방지한다.
트랜잭션 범위
- 작을수록 좋다.
- 하나의 트랜잭션에서 세 개의 테이블을 수정하면 잠금 대상이 많아짐 → 동시에 처리할 수 있는 트랜잭션 개수가 줄어듬 → 전체적인 성능을 떨어뜨림
- 따라서 동일하게 한 트랜잭션에서는 한 개의 애그리거트만 수정해야 함
- 애그리거트는 최대한 서로 독립적이어야 하는데 한 애그리거트가 다른 애그리거트의 기능에 의존하기 시작하면 애그리거트 간 결합도가 높아진다.
- 부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트를 수정해야 한다면 애그리거트에서 다른 애그리거트를 직접 수정하지 말고 응용 서비스에서 두 애그리거트를 수정하도록 구현한다.
도메인 이벤트를 사용하면 한 트랜잭션에서 한 개의 애그리거트를 수정하면서도 동기나 비동기로 다른 애그리거트의 상태를 변경하는 코드를 작성할 수 있다.
리포지터리와 애그리거트
- 객체의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재
ID를 이용한 애그리거트 참조
public class Order {
private Orderer orderer;
...
}
public class Orderer {
private Member member;
private String name;
...
}
public class Member {
...
}
- 필드를 이용한 애그리거트 참조를 사용하면 다른 애그리거트의 데이터를 쉽게 조회할 수 있음
문제점
- 편한 탐색 오용
- 애그리거트 내부에서 다른 애그리거트 객체에 접근할 수 있으면 구현의 편리함 때문에 다른 애그리거트를 수정하고자 하는 유혹에 빠지기 쉽다.
- 성능에 대한 고민
- 즉시 로딩과 지연 로딩 방식 선택에 대한 고민
- 확장 어려움
- 하위 도메인마다 다른 종류의 DBMS를 사용하는 경우도 있음 → 애그리거트 루트를 참조하기 위해 JPA와 같은 단일 기술을 사용할 수 없음
public class Order {
private Orderer orderer;
...
}
public class Orderer {
private MemberId memberId;
private String name;
...
}
public class Member {
private MemberId id;
...
}
- 애그리거트 간 물리적인 연결을 제거하기 때문에 모델의 복잡도를 낮춰준다.
- 애그리거트 간의 의존을 제거하므로 응집도를 높여주는 효과도 있다.
- ID 참조 방식을 사용하면서 N+1 조회와 같은 문제가 발생하지 않도록 하려면 조회 전용 쿼리를 사용하면 된다.
애그리거트 간 집합 연관
- 1 대 N 관계 보단 N 대 1로 연관을 짖자.
- M 대 N 관계라면 중간 테이블을 생성하자
애그리거트를 팩토리로 사용하기
public class RegisterProductService {
public ProductId registerNewProduct(NewProductRequest req) {
Store stroe = storeRepository.findById(req.getStoreId());
checkNull(store);
if(store.isBlocked()) {
throw new StoreBlockedException();
}
ProductId id = productRepository.nextId();
Product product = new Product(id, store.getId(), .. );
productRepository.save(Product);
return id;
}
- Store가 Product를 생성할 수 있는지를 판단하고 Product를 생성하는 것은 논리적으로 하나의 도메인 기능인데 이 도메인 기능을 응용 서비스에서 구현하고 있다.
public class Store {
public Product createProduct(ProductId newProductId , .. ) {
if(isBlocked()) throw new StoreBlockedException();
return new Product(newProductId, getId(), ..);
}
}
}
- 애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면 애그리거트에 팩토리 메서드를 구현하는 것을 고려해 보자.
- Store 애그리거트가 Product 애그리거트를 생성할 때 많은 정보를 알아야 한다면 Store 애그리거트에서 Product 애그리거트를 직접 생성하지 않고 다른 팩토리에 위임하는 방법도 있다.