[DDD-START] Ch03. 애그리거트


공부하는 내용을 정리하는 목적으로 작성하고 있습니다. 잘못 작성된 내용을 지적해주시면 좀더깊이 공부해서 내용을 수정하겠습니다.

애그리거트

시스템을 설계할 때, 너무 세세하게 분리하기 시작하면 전체 구조가 잘 보이지 않는다.

  • 예를들어, 지도를 볼 때 확대해서 보면 어떤 건물, 도로가 있는지는 잘보이지만 해당지역이 어떻게 구성되어있는지는 알아보기 힘들다.
  • 따라서 최초 설계는 상위개념으로 확실히 분리되는것으로 모델들을 정리하는게 전체구조를 파악하기 좋다.
  • 쇼핑몰 주문시스템은 아래와 같이 간단하게 나눌 수 있다.
    Alt text

상위수준으로 분리하여 전체적인 구조의 윤곽이 잡혔다면, 객체 수준으로 낮추어 좀 더 상세히 관계를 분리한다.

  • 연관있는 객체들끼리는 직접적으로 연결된다.
    Alt text
  • 만약 어떤 객체에 대해 관계를 맺는것이 애매모호하다면(~어디에 속해야하는지 잘 모르겠다면~) 구현할 때도 고민할 수 있게 된다.

이 때는 관련있는 객체들을 하나로 묶어서 집합구조로 만드는게 이해하기 편한데, 이것을 애그리거트라고 한다.

  • 위의 객체들간 관계에 애그리거트 개념을 추가하면 아래와 같이 구조를 갖게 될 수 있다.
    Alt text
  • 예를들어, 주문 애그리거트가 생성되려면 주문, 주문자, 배송정보, 주문항목 등의 객체가 필요하다.
    • 주문자가 없는 주문, 배송지가 없는 주문, 주문항목이 없는 주문은 존재할 수 없다.
  • 관련있는 객체들이 모여있으므로, 한 애그리거트에 속한 객체들은 유사한 라이프사이클을 갖게 된다.
  • 애그리거트가 다르다는 것은 직접적인 관련이 없는, 즉 독립적인 라이프사이클을 갖는다고 할 수 있으며, 이것은 애그리거트 간 경계를 갖게 된다고 할 수 있다.
  • 경계는 도메인규칙과 요구사항에 따라 구분짓게 된다.

잘못된 애그리거트

  • 상품과 상품리뷰라는 객체가 있을 때, 이 둘은 항상 붙어있으므로 같은 애그리거트에 속할 수 있다고 생각할 수 있다.
  • 하지만 둘은 변경주체가 다르다. (상품=판매자, 리뷰=구매자)
    • 구매자가 리뷰를 변경한다고해서 이것은 상품에 영향을 1도 줄수없다.
    • 판매자가 상품정보를 변경한다고해서(~가격변경, 내용변경 등등~) 리뷰에 영향을 끼치지 않는다.
  • 대부분의 애그리거트는 1개의 엔티티와 엔티티의 속성을 표현할 수 있는 다수의 밸류들로 포함된다.

애그리거트 루트

  • 애그리거트에 속한 객체들의 상태가 무결성을 보장하려면 외부에서 객체들에게 접근을 허용해선 안된다.
    • 주문 객체(엔티티)를 통하지 않고 외부에서 직접 주문항목의 개수를 변경하거나 배송지 정보를 변경해버리면 주문 객체가 갖고있는 주문정보와 변경된 객체의 정보가 불일치하는 문제가 발생할 수 있다.
  • 따라서 외부에서 애그리거트에 접근하려면 반드시 루트 애그리거트를 통해서만 접근을 허용해야 한다.
    • 이것은 인터페이스(public method)로 구현될 수 있는데 이것들이 결국 애그리거트 루트의 도메인 기능이 된다.
      • 외부에 접근을 허용하는 인터페이스는 반드시 명확한 이름으로 제공되야 한다. (setter X)
  • 애그리거트 루트를 통해서만 외부에 접근을 허용하려면 아래 2가지 방법론이 필요하다.
    1. 도메인 기능에 해당되는 메서드는 public으로 구현하고, 필드를 변경하는 메서드는 private setter로 구현한다.
    2. 밸류는 불변 객체로 구현한다.
      • 밸류 객체를 불변으로 구현해야만 외부에서 애그리거트 내부의 값을 수정할 수 없어 애그리거트의 무결성을 보장된다.

트랜잭션

  • 하나의 트랜잭션에서 다수의 테이블을 변경하게되면 락의 범위가 넓어져 성능상 이슈가 발생할 수 있다.
  • 따라서 하나의 트랜잭션은 하나의 테이블만 수정하도록 범위를 최소화 할 필요가 있다.
  • 한 애그리거트는 대부분 하나의 엔티티를 갖게되므로, 한 트랙잭션은 한 애그리거트만 수정하게 된다. (충돌가능성 줄어듦)
    • 예를들어, 주문 애그리거트에서 회원이나 상품 애그리거트를 수정하면 안된다.
    • 다른 애그리거트의 수정이 가능하다는것은 결국 두 애그리거트 간의 결합도를 높이게되고 이것은 수정이 어려워져 유지보수가 힘들어진다.
  • 만약 한 트랜잭션에서 2개 이상의 애그리거트를 수정하고 싶다면, 도메인 레벨에서 수정하지 말고 한단계 위인 응용서비스 계층에서 각각 애그리거트를 수정하도록 한다.

리포지터리와 애그리거트

  • 애그리거트는 완전한 하나의 도메인 모델을 표현하므로, 엔티티의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재한다.

    • 주문과 주문항목을 별도의 테이블로 구성한다고해서 리포지터리를 둘로 나누지 않는다. (애그리거트 루트에만 의존한다.)
  • 애그리거트를 영속화하고, 조회하기 위해서는 최소 2개의 메서드가 필요하다.
    1. save: 애그리거트의 영속화
    2. findById: 애그리거트 조회
  • 애그리거트를 영속화할 때, 리포지터리는 반드시 애그리거트에 속한 모든 요소들을 저장해야한다.

    ...
    // order 객체 뿐 아니라 order에 종속된 orderLines, orderState, shippingInfo 등이 모두 저장되어야 한다.
    orderRepository.save(order);
    
  • 애그리거트를 조회할 때, 리포지터리는 반드시 애그리거트에 속한 모든 요소들을 조회해야한다.

    ...
    // order 객체는 order 뿐 아니라 종속된 orderLines, orderState, shippingInfo 등이 모두 함께 조회되어야 한다.
    // 만약 하위 객체들이 제대로 조회되지 않는다면 NullPointerException이 발생하게 된다.
    Order order = orderRepository.findById(orderId);
    

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

  • 객체 레퍼런스를 통해 다른 애그리거트의 엔티티를 참조하게 될 경우, 몇 가지 문제가 발생할 수 있다.
    1. 해당 객체(엔티티)가 수정될 수 있는 문제가 있다.
    2. 해당 엔티티가 수정될 경우, 의존하고 있는 애그리거트도 수정될 수 있으므로 확장이 어려워진다.
    3. 로딩방식에 따라(지연로딩/즉시 로딩) 성능이 좌우될 수 있으므로 요구사항에 따른 고민이 많이 필요해진다.
  • 따라서 같은 애그리거트 내에서 참조는 객체 레퍼런스를 사용하더라도, 다른 애그리거트의 참조는 ID를 통한 참조로 구현한다.

    ... 
    // 주문 애그리거트에서 회원(구매자) 애그리거트의 잘못된 참조 (배송지 주소 변경)
    // 주문과 회원 애그리거트가 orderRepository에 종속되므로 두 애그리거트는 서로 다른 DBMS를 사용하기 어려워진다.
    orderer.getMember().changeAddress(shippingInfo.getAddress());
    
    ...
    // 주문 애그리거트에서 회원(구매자) 애그리거트의 잘된 참조 (배송지 주소 변경)
    // 주문과 회원 애그리거트가 각각 다른 리포지터리를 사용하므로 향후 두 애그리거트는 서로 다른 DBMS를 사용할 수 있다.
    Member member = memberRepository.findById(orderer.getMemberId());
    member.changeAddress(shippingInfo.getAddress());
    

애그리거트 팩토리 사용

  • 한 애그리거트에서 다른 애그리거트를 생성해야 하는경우 직접생성하면 두 애그리거트 간 의존성이 높아진다.
  • 판매자의 차단상태를 체크하여 상품생성을 결정짓는 기능이 있다고 가정할 때, 아래와 같은 문제가 발생할 수 있다.
    1. 판매자의 차단상태를 체크하는것(회원 애그리거트)과 판매자가 상품을 생성하는것(상품 애그리거트)은 서로 다른 도메인이므로 응용 서비스에서 처리하게 된다.
    2. 판매자의 상태체크를 하거나 새로운 상품을 생성하는 비즈니스 로직은 도메인의 기능인데 이것이 응용 서비스에 노출된다.
    3. 응용 서비스에서 직접 상품을 생성하게 되면(new) 서비스와 상품 객체 간 의존성이 높아져, 추후 확장에 어려움이 발생할 수 있다.
  • 따라서 판매자라는 별도의 객체를 생성하고 두 기능을 판매자 객체가 처리하도록 한다.
    • 이렇게되면 응용서비스에서 판매자의 도메인 기능이 노출되지 않으므로 확장에 용이해진다.

      ...
      public class Store extends Member {
        public Product createProduct(ProductId id, ...) {
            if (blocked()) { ... }
            return new Product(id, ...);
        }
      }
      
      ...
      public class RegisterProductServicve {
        public ProductId registerNewProduct(NewProductRequest request) {
            Strong acount = accountRepository.findById(request.getStoreId());
            checkNull();
            productId id = productRepository.nextId();
            // RegisterProductServicve에서 new Product(...);를 하지 않으므로 결합도가 약해진다.
            Product product = acount.createProduct(id, ...);
            productRepository.save(product);
        }
      }
      

DDD-START (최범균님 저) 도서 참조






© 2020.09.23 by chpark

Powered by chpark