DDD 애그리거트

애그리거트

  • 복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만들려면 상위 수준에서 모델을 조망할 수 있는 방법이 필요한데, 그 방법이 애그리거트다. 수많은 객체를 애그리거트로 묶어서 바라면 상위 수준에서 도메인 모델 간의 관계를 파악할 수 있다.
  • 애그리거트는 복잡한 도메인을 단순한 구조로 만들어준다. 복잡도가 낮아지는 만큼 도메인 기능을 확장하고 변경하기 쉬워진다.
  • 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 가진다.
  • 애그리거트는 경계를 갖는다. 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다. 독립된 객체 군이며 다른 애그리거트를 관리하지 않는다.
  • 도메인에 대한 이해도가 증가할수록 애그리거트의 실제 크기는 줄어든다. 한 개의 엔티티 객체만 갖는 애그리거트가 많으며, 두 개 이상의 엔티티로 구성되는 애그리거트는 드물다.

애그리거트 루트

  • 애그리거트 전체를 관리할 주체. 애그리거트의 대표 엔티티.
  • 핵심 역할은 애그리거트의 일관성이 깨지지 않도록 하는 것.
  • 애그리거트 루트는 애그리거트 내부의 다른 객체를 조합해서 기능을 완성한다.

트랜잭션 범위

  • 트랜잭션에서 잠금 대상이 많아진다는 것은 그만큼 동시에 처리할 수 있는 트랜잭션 개수가 줄어든다는 것을 의미한다. 이는 전체적인 처리량을 떨어뜨린다.
  • 한 트랜잭션에서는 한 개의 애그리거트만 수정해야 한다. 한 트랜잭션에서 두 개 이상의 애그리거트를 수정하면 트랜잭션 충돌이 발생할 가능성이 더 높아진다.
  • 애그리거트는 최대한 독립적이어야 한다. 다른 애그리거트의 기능에 의존하기 시작하면 결합도가 높아지고 향후 수정 비용이 증가하게 되므로 다른 애그리거트의 상태를 변경하지 말아야 한다.
  • 부득이하게 한 트랜잭션에서 두 개 이상의 애그리거트를 수정해야 한다면 응용 서비스에서 두 애그리거트를 수정해야 한다.

리포지터리

  • 객체의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재한다.
  • 애그리거트를 구하는 Repository 메서드는 완전한 애그리거트를 제공해야 한다. 예를 들어 주문 애그리거트는 주문자, 주문상품 등 모든 구성요소를 포함하고 있어야 한다.
Order order = orderRepository.findById(orderId);

// order가 온전한 애그리거트가 아니면 NullPointException과 같은 문제가 발생한다.
order.cancel();

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

  • 한 객체가 다른 객체를 참조하는 것처럼 애그리거트도 다른 애그리거트를 참조한다. 애그리거트 관리 주체는 애그리거트 루트이므로 다른 애그리거트를 참조한다는 것은 다른 애그리거트의 루트를 참조하는 것이다.
  • ORM 기술로 애그리거트 루트에 대한 참조를 쉽게 구현할 수 있고 필드를 이용한 애그리거트 참조를 사용하면 다른 애그리거트의 데이터를 쉽게 조회할 수 있다.

필드를 이용한 애그리거트 참조시 주의사항

  • 편한 탐색 오용 : 한 애그리거트가 관리하는 범위는 자기 자신으로 한정해야 한다. 애그리거트 내부에서 다른 애그리거트 객체에 접근할 수 있으면 편리함 때문에 다른 애그리거트를 수정하고자 하는 유혹에 빠지기 쉽다. 다른 애그리거트의 상태를 변경하는 것은 애그리거트 간 의존 결합도를 높여서 결과적으로 애그리거트 변경을 어렵게 만든다.
  • 성능에 대한 고민 : JPA 사용 시 Lazy 로딩과 Eager 로딩 중 연관된 객체의 데이터를 함께 화면에 보여줘야 하면 Eager 로딩, 애그리거트의 상태를 변경하는 기능을 실행하는 경우에는 불필요한 객체를 로딩할 필요없이 Lazy 로딩이 유리하다. 다양한 경우의 수를 고려해서 연관 매핑과 쿼리의 로딩 전략을 결정해야 한다.
  • 확장 어려움 : 트래픽이 증가하면 부하를 분산하기 위해 하위 도메인별로 시스템을 분리하기 시작한다. 이 과정에서 하위 도메인마다 서로 다른 DBMS를 사용할 때도 있다. 심지어 하위 도메인마다 다른 종류의 데이터 저장소를 사용하기도 한다. 이것은 더 이상 다른 애그리거트 루트를 참조하기 위해 JPA와 같은 단일 기술을 사용할 수 없음을 의미한다.

ID를 이용한 애그리거트 참조의 장점

위 세가지 문제를 완화할 때 사용할 수 있는 것이 ID를 이용해서 다른 애그리거트를 참조하는 것이다.

  • ID 참조를 사용하면 모든 객체가 참조로 연결되지 않고 한 애그리거트에 속한 객체들만 참조로 연결된다.
  • 물리적 연결을 제거하기 때문에 모델의 복잡도를 낮춰준다.
  • 애그리거트 간의 의존을 제거하므로 응집도를 높여준다.
  • 애그리거트별로 다른 구현 기술을 사용하는 것이 가능해진다. 예를 들어 주문 애그리거트는 RDBMS, 조회 성능이 중요한 상품 애그리거트는 NoSQL에 저장할 수 있다.

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

public class Store {
    // Store 애그리거트에서 Product를 생성
    public Product createProduct(ProductId newProductId, ...) {
        if (isBlocked()) 
            throw new StoreBlockedException();
        
        return new Product(newProductId, getId(), ...);
    }
}
  • Store 애그리거트의 createProduct() 는 Product 애그리거트를 생성하는 팩토리 역할을 한다. 팩토리 역할을 하면서도 중요한 도메인 로직을 구현하고 있다.
  • Store가 Product를 생성할 수 있는지를 확인하는 도메인 로직은 Store에서 구현하고 있다. 이제 Product 생성 가능 여부를 확인하는 도메인 로직을 변경해도 도메인 영역의 Store만 변경하면 되고 응용 서비스는 영향을 받지 않는다. 그러므로 도메인의 응집도가 높아진다.
  • 애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면 애그리거트에 팩토리 메서드를 구현하는 것을 고려해 보자.
public class Store {
    // Store 애그리거트가 Product 애그리거트를 생성할 때 많은 정보를 알아야 한다면 Store 애그리거트에서 Product 애그리거트를 직접생성하지 않고 다른 팩토리에 위임하는 방법도 있다.
    public Product createProduct(ProductId newProductId, ProductInfo pi) {
        if (isBlocked())
            throw new StoreBlockedException();

        return ProductFactory.create(newProductId, getId(), pi);
    }
}
Reference
  • 도메인 주도 개발 시작하기, 최범균 지음

Categories:

Updated:

Comments