고딩왕 코범석

3장 애그리거트 본문

Book Lounge/도메인 주도 개발 시작하기

3장 애그리거트

고딩왕 코범석 2022. 7. 2. 17:38
반응형

Index

  1. 애그리거트
  2. 애그리거트 루트
  3. 리포지터리와 애그리거트
  4. ID를 이용한 애그리거트 참조
  5. 애그리거트 간 집합 연관
  6. 애그리거트를 팩토리로 사용하기

애그리거트

  • 애그리거트는 관련된 모델들을 하나로 모았기 때문에 한 애그리거트에 속한 객체는 유사하거나 동일한 라이프사이클을 갖게 된다.
  • 애그리거트는 경계를 갖게 되며, 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다.
    • 예를 들어, 주문 애그리거트에서 배송지 혹은 상품 갯수를 변경할 수 있지만 회원의 정보를 변경할 수 없다.
  • 도메인 규칙에 따라 함께 생성되는 구성요소는 한 애그리거트에 속할 가능성이 높다.
    • 하지만 ‘A가 B를 갖는다’ 라는 요구사항이 있다고 해서 A와 B가 반드시 한 애그리거트에 속하는 것은 아니다.
      • 예를 들어, 하나의 상품이 등록되어야 리뷰를 작성한다고 가정했을 때 꼭 상품 애그리거트에 리뷰가 들어가지 않아도 된다. 상품과 리뷰는 함께 생성되지 않고 함께 변경되지도 않기 때문이다.

애그리거트 루트

도메인 규칙과 일관성

  • 애그리거트에 속한 모든 객체는 정상 상태와 일관성을 유지해야한다.
  • 하나의 도메인에 대해 기능이 있다면 애그리거트 루트를 통해 호출한다.
    • 응용 계층과 같은 애그리거트 밖에서 정보를 직접 변경하면 안된다.
    • 일관성이 깨지기 쉽고 코드가 흩어지게되는 원인이 된다. 따라서 유지보수도 힘들어진다.
  • 애그리거트는 기능 수행 뿐만 아니라 기능 실행을 위임하기도 한다.

트랜잭션 범위

  • 하나의 트랜잭션에서 수행되는 SQL이 적을수록 좋다.
    • 수정시 수행되는 SQL이 적어야 Lock이 발생하는 row도 적기 때문이다.
  • 만약 애그리거트가 자신의 책임 범위를 넘어 다른 애그리거트를 수정한다면 애그리거트 간 결합도가 높아진다.
    • 결합도가 높아질수록 향후 수정 비용이 증가한다.
  • 한 트랜잭션 내에 두 개의 애그리거트를 수정해야한다면 Application 영역에서 두 애그리거트를 수정하도록 구현한다.

리포지터리와 애그리거트

  • 리포지터리는 한 애그리거트의 영속성을 책임진다. 즉, 애그리거트 별 한개의 리포지터리가 존재한다.
    • OrderOrderLine이 별도의 DB 테이블이 있다 하더라도 하나의 OrderRepository에서 해결한다.
    • RDBMS 사용시 트랜잭션을 이용해 애그리거트의 변경이 저장소에 반영되는 것을 보장할 수 있다.
    • MongoDB 사용시 한개의 애그리거트를 문서로 저장하여 변경을 손실없이 저장한다.

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

  • 애그리거트도 다른 애그리거트를 참조하는데, 참조하는 애그리거트는 해당 애그리거트의 루트 객체이다.

애그리거트를 직접 참조할 때 단점

  1. 편리함의 오용
    • 한 애그리거트가 관리하는 범위는 자기 자신으로 한정해야한다.
    • 애그리거트를 직접 참조하게되면 자신이 다른 애그리거트를 수정할 수 있기 때문이다.
  2. 성능상의 이슈
    • JPA를 사용하면 즉시 로딩과 지연 로딩 전략이 존재하는데, 조회 시에는 즉시 로딩이 빠르지만 수정 시에는 불필요한 객체를 함께 가져와서 성능상에 문제가 생긴다.
  3. 확장성
    • 규모가 점점 커져 하위 도메인별로 시스템을 분리할 때, 다른 데이터 저장소를 사용할 수 있다.
    • 만약 JPA를 사용하다가 특정 도메인의 데이터 저장소를 NoSQL로 변경한다면 JPA의 연관 관계 설정을 할 수 없게되어 확장성이 부족해진다.

ID를 이용해서 다른 애그리거트를 참조하기

  • ID를 참조하게되면 모든 객체가 참조로 연결되지 않고 한 애그리거트에 속한 객체들로만 참조로 연결된다.
  • 또 다른 애그리거트는 그 애그리거트의 ID를 참조하기 때문에 경계가 명확해지며 응집도가 높아지고 복잡도를 낮춰준다.
  • 이를 응용 서비스에서 호출하기 때문에 지연 로딩과 동일한 효과를 누릴 수 있다.

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

  • 서로 다른 애그리거트의 객체가 아닌 ID로 참조를 하게 되면 N + 1 문제가 발생할 수 있다.
    • 하지만 객체를 직접 참조하게 되면 결합도가 높아지는 단점이 다시 발생한다.
  • 이를 해결하기 위해 별도의 조회 DAO를 만들어서 한번에 데이터를 가져오는 방식이 적합하다.
  • 만약 애그리거트 별 데이터 저장소가 다르다면 캐시를 적용하거나 조회 전용 저장소를 따로 구성한다.

애그리거트 간 집합 연관

1:N 연관 관계의 경우

  • 아래 코드를 보면 하나의 Category가 여러 Product를 가진다는 것을 도메인에 표현했다.
public class Category {

    private Set<Product> products;

    public List<Product> getProducts(int page, int size) {
        // logic
    }
...
}
  • 이 경우 하나의 카테고리에 모든 상품을 조회하기 때문에 Product가 엄청 많이 있다면 성능 상의 이슈가 발생한다. 개념적으로 1:N 관계더라도 애그리거트에서 실제 물리적으로 1:N 관계를 표현하지 않는다.
  • 위의 예제에서 카테고리가 상품을 몰라도 된다면 아래와 같이 N:1로 연관지으면 된다.
public class Product {

    private CategoryId categoryId;

...
}

M:N 연관 관계의 경우

  • 상품과 카테고리가 M:N인 경우 상품 목록 조회 시 상품 마다 어떤 카테고리인지는 몰라도 된다.
  • 상품이 어떤 카테고리에 속해있는지 알아야하는 경우는 상품 상세 조회 기능일 것이다.
  • 따라서 Product에 CategoryIds를 참조하는 방식으로 설정한다. 또한 RDB에서는 조인 테이블을 사용할텐데, 이를 밸류 타입의 CategoryId를 컬렉션으로 참조한다.
@Entity
public class Product {

    ...
    @ElementCollection
    @CollectionTable(
        name = "product_category",
        joinColumns = @JoinColumn(name = "product_id")
    )
    private Set<CategoryId> categoryIds;
    ...
}

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

  • 응용 서비스에서 도메인 로직 처리 과정에 대해 코드로 구현하고 있다면 상위 애그리거트에서 팩토리 메서드로 구현할 수 있다.
    • 예를 들어, Store에서 Product를 생성한다 가정했을 때, Store가 차단 상태인지 검사하고 요청 정보를 통해 Product 객체를 생성 후 저장하는 과정이 응용 서비스에 있는 경우라고 가정해본다.
    • Store의 상태에 따라 Product를 만들 수 있는지 좌우되므로 Store 엔티티에서 Product 객체를 만드는 팩토리 메서드를 구현해볼 수 있다.
    • 이렇게 되면 응용 서비스에 노출된 검증하고 생성하는 로직이 자연스레 사라지게 된다.
  • 정리하자면, 애그리거트가 갖고 있는 데이터를 이용해 다른 애그리거트를 생성해야한다면 애그리거트에 팩토리 메서드 구현을 고려해보기.
반응형