# 3. 애그리거트

# 3.1 애그리거트

  • 상위 수준에서 모델을 정리하면 복잡한 도메인 모델을 이해하는데 도움이 됨
  • 개별 객체 수준에서 모델을 바라보면 관계 파악이 어려움 → 코드 변경 및 확장이 어려움

애그리거트란 상위 수준에서 모델을 조망하는 방법

  • 애그리거트에 속한 구성요소는 대부분 함께 생성하고 함께 제거
  • 애그리거트는 경계를 가짐 → 한 애그리거트에 속한 개체는 다른 애그리거트에 속하지 않음

# 애그리거트 경계 설정 시

  • 도메인 규칙과 요구사항
  • 도메인 규칙에 따라 함께 생성되는 구성요소는 한 애그리거트에 속할 가능성이 높음
  • ex) 주문할 상품 개수, 배송지 정보, 주문자 정보
  • A가 B를 가질 때 A와 B는 같은 애그리거트에 속한다 → X
  • ex) 상품 - 리뷰. 상품 상세에 들어갔을 때 리뷰를 같이 보여줘야 한다면 한 애그리거트에 속한다고 생각할 수 있지만 product와 review는 함께 생성되지도 않고 함께 변경되지도 않는다

# 3.2 애그리거트 루트

애그리거트는 여러 객체로 구성되기 때문에 도메인 규칙을 지키려면 애그리거트에 속한 모든 객체가 정상 상태를 가져야 함

  • 애그리거트 전체를 관리할 주체 → 애그리거트의 루트 엔티티 (애그리거트의 대표 엔티티 )

# 3.2.1 도메인 규칙과 일관성

  • 애그리거트 루트의 핵심 역할은 애그리거트의 일관성이 깨지지 않도록 하는 것 → 애그리거트가 제공해야 할 도메인 기능 구현
  • 애그리거트 내부에서만 에그리거트에 속한 객체를 변경할 수 있도록
public class Order {

    private ShippingInfo shippingInfo;

    // 의미가 드러나는 메서드명 사용
    public void changeShippingInfo(ShippingInfo newShippingInfo) {
        verifyNotYetShipped();
        setShippingInfo(newShippingInfo);
    }

    // set 메서드는 private로
    private void setShippingInfo(ShippingInfo newShippingInfo) {
        this.shippingInfo = newShippingInfo;
    }
}
  • 애그리거트 외부에서 내부 상태를 함부로 바꾸지 못하므로 일관성이 깨질 가능성이 줄어듦

# 애그리거트 루트의 기능 구현

  • 애그리거트 루트는 내부의 다른 객체들을 조합해서 기능 완성
  • ex) Order는 총 주문금액을 완성하기 위해 OrderLine 목록 사용
  • 상태 참조 or 상태 변경 위임

# 상태 변경 위임 예시

public class OrderLines {

    private List<OrderLines> lines;

    public Money getTotalAmounts() {...}

    public void changeOrderLines(List<ORderLine> newLines) {
        this.lines = newLines;
    }

}

public class Order {

    private OrderLines orderLines;

    public void changeOrderLines(List<OrderLine> newLines) {
        orderLines.changeOrderLines(newLines);
        this.totalAmounts = orderLines.getTotalAmounts();
    }
}
  • 근데 이렇게 하면 totalAmount를 계산하지 않는 버그가 생길 수도 있음
  • 해겳법은 애그리거트 외부에서 OrderLine 목록을 변경할 수 없도록 불변 (final, setter 없음, protected 등)로 구성

# 3.2.3 트랜잭션 범위

  • 한 트랜잭션에서 한 애그리거트만 수정
  • 애그리거트에서 다른 애그리거트를 변경하지 않음
  • 부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트를 수정해야 한다면 응용 서비스에서 두 애그리거트를 수정하도록 구현
public class ChangeOrderService {

    @Transactional
    public void changeShippingInfo(...) {
        Order order = orderRepository.findById(id);
        if (order == null) {
            throw new OrderNotFoundException();
        }

        order.shipTo(newShippingInfo);
        if (useNewShippingAddrAsMemberAddr) {
            order.getOrderer().getCustomer().changeAddress(newShippingInfo.getAddress());
        }
    }
}
  • 도메인 이벤트로 한 애그리거트를 수정하면서 다른 애그리거트의 상태를 변경할 수도 있음

# 한 트랜잭션에서 두 개 이상의 애그리거트를 수정하는 경우

  • 응용 서비스의 기능을 한 트랜잭션으로 실행해야 하는 경우
  • 기술적으로 이벤트 방식을 도입할 수 없는 경우
  • 운영ㅇ의 편리함을 위해 한 트랜잭션에서 여러 애그리거트의 상태 변경

# 3.3 리포지터리와 애그리거트

  • 리포지터리는 애그리거트 단위로 존재

Q. P.112에 JPA에서 밸류 타입을 매핑할 때 사용하는 @Component 라는데 @Embaddable 아님?

  • 애그리거트 저장 시 해당 애그리거트에 속한 모든 구성요소를 포함해야 함
  • 그렇지 않으면 기능 실행 도중 문제가 발생할 수도 있음

# 3.4 ID를 이용한 애그리거트 참조

  • 한 애그리거트 루트가 다른 애그리거트 루트를 참조 가능
  • 근데 ORM 기술 사용 시 여러 문제가 생길 수 있음
  1. 다른 애그리거트에 쉽게 접근할 수 있으므로 다른 애그리거트를 수정하고자 하는 유혹에 빠지기 쉬움 → 다른 애그리거트에 대해 의존 결합도를 높임
  2. 성능과 관련된 여러가지 고민이 생김
  3. 서비스가 커지면서 도메인별로 시스템을 분리하기 어려워짐 → ID를 이용해서 다른 애드리거트를 참조하도록
  • 애그리거트의 경계를 명확히 하고
  • 애그리거트 간 물리적인 연결을 제거 → 모델의 복잡도를 낮춤
  • 애그리거트 간 의존 제거 → 응집도를 높히는 효과
  • 구현 복잡도도 낮아짐 → 필요하면 응용 서비스에서 ID를 이용해서 로딩

# 3.4.1 ID를 이용한 참조와 조회 성능

  • 애그리거트 참조로 N+1 조회문제가 생길 수 있음
  • 조회 전용 쿼리 사용으로 해결 가능
@Repository
public class JpaOrderViewDao implements OrderViewDao {
    @PersistenceContext
    private EntityManager em;

    @Override
    public List<OrderView> selectByOrder(String ordererId) {
        String selectQuery =
            "select new com.myshop.order.application.dto.OrderView(o, m, p) " +
            "from Order o join o.orderLines ol, Member m, Product p " +
            "where o.orderer.memberId.id = :ordererId " +
            "and o.orderer.memberId = m.id " +
            "and ol.productId = p.id " +
            "order by o.number.number desc";

        TypedQuery<OrderView> query =
            em.createQuery(selectQuery, OrderView.class);
        query.setParameter("ordererId", ordererId);
        return query.getResultList();
    }
}
  • 애그리거트마다 다른 저장소를 사용하여 한번에 로드하지 못하는 경우 → 조회 성능을 높히기 위해 캐시를 도입하거나 조회 전용 저장소를 따로 구성

# 3.5 애그리거트 간 집합 연관

  • 성능 문제가 발생할 수 있기 때문에 애그리거트간 연관이 있더라도 실제 구현에 반영하지 않을 수 있음
  • n-m 관계에서도 상품-카테고리 관계에 대해 상품에서 카테고리로의 단방향만 적용
@Entity
@Table(name="product")
public class Product{
	@EmbeddedId
	private ProductId id;
	
	@ElementCollection
	@CollectionTable(name = "product_category",
						joinColumns = @JoinColumn(name = "product_id"))
	private Set<CategoryId> categoryIds;
...
}

Q. 이러면 그럼 테이블이 product, category, product_category 이렇게 있는데 product-product_category 이렇게만 연결해서 쓰는 느낌이고 category는 연결 안된 느낌?

# 3.6 애그리거트를 팩토리로 사용하기

  • 한 애그리거트를 다른 애그리거트를 생성하는 팩토리 역할로도 사용 가능
  • ex) Store 애그리거트에서 Product 생성할 수 있는 상태(차단 여부 등)인지 확인 후 Product 애그리거트 생성
  • 애그리거트가 갖고 있는 데이터를 사용해서 다른 애그리거트를 생성해야 한다면 애그리거트에 팩토리 메서드를 구현하는 것 고려해보기
  • 혹은 다른 정보가 너무 많이 필요하다면 또 다른 팩토리에 위임도 가능

Q. 그럼 위임한걸 또 위임해도 됨? 예를 들어서 store에서 업로그 가능한지 확인 후 category로 위임해서 category 팩토리에서 생성