# Chapter 3: 애그리거트

<br>

## 💡 책에서 기억하고 싶은 내용

<br>

### 애그리거트

* `상위 수준`에서 전체 모델을 정리하면 도메인 모델의 복잡한 관계를 이해하는 데 도움이 된다
  * 개별 객체 수준에서 모델을 바라보면 상위 수준에서 관계를 파악하기 어렵다
* 복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만들려면 상위 수준에서 모델을 조망할 수 있는 방법이 필요한데, 그 방법이 바로 `애그리거트`다!
  * 애그리거트는 `복잡한 모델` 을 `관리하는 기준` 을 제공한다
  * `일관성`을 관리하는 기준도 된다
* 애그리거트는 관련된 모델을 하나로 모았기 때문에, 한 애그리거트에 속한 객체는 `유사하거나 동일한 라이프 사이클`을 갖는다
  * 애그리거트에 속한 구성요소는 대부분 함께 생성하고 함께 제거한다
* 애그리거트는 `경계`를 갖는다
  * 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다
  * 애그리거트는 `독립된 객체 군`이며, 각 애그리거트는 `자신을 관리`할 뿐 다른 애그리거트를 관리하지 않는다
* `경계`를 설정할 때 기본이 되는 것은 `도메인 규칙`과 `요구사항`이다
  * 도메인 규칙에 따라 `함께 생성/변경되는 구성요소` 는 한 애그리거트에 속할 가능성이 높다
  * 흔히 ‘A가 B를 갖는다' 로 설계할 수 있는 요구사항이 있다면 A와 B를 한 애그리거트로 묶어서 생각하기 쉽지만, 반드시 A와 B가 한 애그리거트에 속하는 것은 아니다!
* 다수의 애그리거트가 한 개의 엔티티 객체만 갖는 경우가 많고, 두 개 이상의 엔티티로 구성되는 애그리거트는 드물다… 는 저자의 경험담

### 애그리거트 루트

* 애그리거트는 여러 객체로 구성되기 때문에 한 객체만 상태가 정상이면 안 된다!
  * 도메인 규칙을 지키려면 애그리거트에 속한 모든 객체가 정상 상태를 가져야 한다
* 애그리거트에 속한 모든 객체가 `일관된 상태`를 유지하려면 애그리거트 전체를 관리할 주체가 필요한데, 이 책임을 지는 것이 애그리거트의 `루트 엔티티`다
  * 루트 엔티티는 애그리거트의 `대표 엔티티`다
  * 애그리거트에 속한 객체는 루트 엔티티에 `직접 or 간접적으로 속하게 된다`
* 애그리거트 루트는 도메인 규칙을 구현한 기능을 제공한다

### 도메인 규칙과 일관성

* 애그리거트의 핵심 역할은 애그리거트의 `일관성`이 깨지지 않도록 하는 것이다
  * 이를 위해 애그리거트 루트는 애그리거트가 제공해야 할 `도메인 기능을 구현`한다
* 애그리거트 루트가 제공하는 method는 `도메인 규칙`에 따라 애그리거트에 속한 객체의 `일관성`이 깨지지 않도록 구현해야 한다
* 애그리거트 외부에서 애그리거트가 속한 객체를 직접 변경하면 안 된다
  * 이렇게 하면 애그리거트 루트가 `강제하는 규칙` 을 적용할 수 없어 모델의 일관성을 깨게 된다
* 불필요한 중복을 피하고 애그리거트 루트를 통해서만 도메인 로직을 구현하게 만들기 위해 도메인 모델에 적용해야 하는 것
  1. 단순히 field를 변경하는 `set method`를 `public`으로 만들지 않는다
     * 공개 set method는 도메인의 의미나 의도를 표현하지 못하고, 도메인 로직을 도메인 객체가 아닌 응용 영역 or 표현 영역으로 분산시킨다
     * 공개 set method를 사용하지 않의면 의미가 드러나는 method를 사용해서 구현할 가능성이 높아진다
       * ex) cancel(), changePassword()
  2. `밸류 타입`은 `불변`으로 구현한다
     * 밸류 객체의 값을 변경할 수 없으면 애그리거트 루트에서 밸류 객체를 구해도, 애그리거트 외부에서 밸류 객체의 상태를 변경할 수 없다
     * 밸류 객체가 불변일 때 밸류 객체의 값을 변경하는 방법은 새로운 밸류 객체를 할당하는 것뿐이다!
       * `밸류 타입`의 `내부 상태`를 변경하려면 `애그리거트 루트`를 통해서만 가능하므로, 애그리거트 루트가 도메인 규칙을 올바르게 구현하면 애그리거트 전체의 `일관성`을 올바르게 유지할 수 있다

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

* 애그리거트는 구성요소의 상태를 참조하는 것 뿐만 아니라, `기능 실행`을 위임하기도 한다
* 보통 한 애그리거트에 속하는 모델은 한 패키지에 속하기 때문에 `package` 나 `protected` 범위를 사용하면 애그리거트 외부에서 상태 변경 기능을 실행하는 것을 방지할 수 있다

### 트랜잭션 범위

* 트랜잭션 범위는 `작을수록` 좋다
  * 잠금 대상이 많아진다는 것은 그만큼 동시에 처리할 수 있는 트랜잭션 수가 줄어든다는 것을 의미하고, 전체적인 성능(처리량)을 떨어뜨린다
* `한 트랜잭션`에서는 `한 개의 애그리거트만` 수정해야 한다
  * 한 트랜잭션에서 두 개 이상의 애그리거트를 수정하면 `트랜잭션 충돌` 이 발생할 가능성이 더 높아지기 때문에, 수정하는 애그리거트 개수가 많아질수록 전체 처리량이 떨어지게 된다
* 한 트랜잭션에서 한 애그리거트만 수정하는 것은 애그리거트에서 `다른 애그리거트를 변경하지 않는다`는 것을 의미한다
  * 다른 애그리거트를 수정하면 결과적으로 두 개의 애그리거트를 한 트랜잭션에서 수정하게 된다
  * 한 애그리거트가 다른 애그리거트의 기능에 의존하기 시작하면 애그리거트 간 `결합도`가 높아진다
* 만약 부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트를 수정해야 한다면, 애그리거트에서 다른 애그리거트를 직접 수정하지 말고 `응용 서비스` 에서 두 애그리거트를 수정하도록 구현한다

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

* 애그리거트는 개념상 `한 개의 도메인 모델` 을 표현하므로 객체의 `영속성`을 처리하는 리포지터리는 `애그리거트 단위`로 존재한다
* 애그리거트는 개념적으로 하나이므로 리포지터리는 `애그리거트 전체`를 저장소에 `영속화`해야 한다
  * ex) Order 애그리거트와 관련된 테이블이 3개면?
    * Order 애그리거트 저장 시 애그리거트 루트와 매핑되는 테이블 + 애그리거트에 속한 모든 구성요소에 매핑된 테이블에 데이터를 저장해야 한다
* 애그리거트를 `영속화`할 저장소로 무엇을 사용하든지 간에 애그리거트의 상태가 변경되면 모든 변경을 `원자적` 으로 저장소에 반영해야 한다
  * 애그리거트에서 두 개의 객체를 변경했는데, 저장소에는 한 객체에 대한 변경만 반영되면 `데이터 일관성`이 깨지므로 문제가 된다!

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

* 애그리거트 관리 주체는 `애그리거트 루트` 이므로 애그리거트에서 다른 애그리거트를 참조한다는 것은 `다른 애그리거트의 루트를 참조`한다는 것과 같다
* 애그리거트 간의 참조는 `field` 를 통해 구현할 수 있다
* `Field`를 이용한 애그리거트 참조는 개발자에게 구현의 편리함을 제공하지만, 아래와 같은 문제를 야기할 수 있다
  1. `편한 탐색 오용`
     * 한 애그리거트 내부에서 다른 애그리거트 객체에 접근할 수 있으면 다른 애그리거트의 상태를 쉽게 변경할 수 있게 된다
     * 한 애그리거트에서 다른 애그리거트의 상태를 변경하는 것은 애그리거트 간의 `의존 결합도` 를 높여서, 결과적으로 애그리거트의 변경을 어렵게 만든다
  2. `성능에 대한 고민`
     * 애그리거트를 직접 참조하면, 성능과 관련된 여러 고민을 해야 한다
     * JPA를 사용하면 참조한 객체를 `지연(lazy) 로딩`과 `즉시(eager) 로딩`의 방식으로 로딩할 수 있는데, 둘 중 무엇을 사용할지는 애그리거트의 어떤 기능을 사용하느냐에 따라 달라진다
       * 단순히 연관된 깨체의 데이터를 함께 화면에 보여줘야 하면, `즉시 로딩` 이 조회 성능에 유리하지만,
       * 애그리거트의 상태를 변경하는 기능을 실행하는 경우에는 불필요한 객체를 함께 로딩할 필요가 없으므로 `지연 로딩` 이 유리할 수 있다
     * 다양한 경우의 수를 고려해서 `연관 매핑` 과 `JPQL/Criteria` 쿼리의 로딩 전략을 결정해야 한다
  3. `확장 어려움`
     * 사용자가 늘고 트래픽이 증가하면 `부하를 분산` 하기 위해 하위 도메인별로 시스템을 분리하기 시작하는데, 이 과정에서 서로 다른 DBMS를 사용할 때도 있다
     * 더 이상 다른 애그리거트 루트를 참조하기 위해 JPA와 같은 단일 기술을 사용할 수 없음을 의미한다
* 위의 문제점을 완화화기 위해 `ID` 를 이용해서 `다른 애그리거트를 참조`할 수 있다
  * DB 테이블에서 외래키로 참조하는 것과 비슷하게, ID를 이용한 참조는 다른 애그리거트를 참조할 때 ID를 이용한다
  * `ID 참조` 를 사용하면 모든 객체가 참조로 연결되지 않고 `한 애그리거트에 속한 객체들만` 참조로 연결된다
* `ID를 이용한 간접 참조의 장점`
  * 애그리거트의 경계를 명확히 하고 애그리거트 간 `물리적 연결` 을 제거하기 때문에 `모델의 복잡도` 를 낮춰준다
  * 애그리거트 간의 `의존을 제거` 하므로 `응집도` 를 높여주는 효과도 있다
    * 애그리거트 별로 `다른 구한 기술` 을 사용하는 것도 가능하다
      * ex)
        * 중요 데이터인 주문 애그리거트는 RDMS에 저장
        * 조회 성능이 중요한 상품 애그리거트는 NoSQL에 저장
    * 각 도메인을 별도 프로세스로 서비스하도록 구현할 수도 있다
      * ID로 애그리거트를 참조하면, `리포지터리마다 다른 저장소`를 사용하도록 구현할 때 확장이 용이하다
  * `구현 복잡도` 도 낮아진다
    * 다른 애그리거트를 직접 참조하지 않으므로, 애그리거트 간 참조를 `지연 로딩` 으로 할지 `즉시 로딩` 으로 할지 고민하지 않아도 된다
    * 참조하는 애그리거트가 필요하면 응용 서비스에서 ID를 이용해서 로딩하면 된다
    * 응용 서비스에서 필요한 애그리거트를 로딩하므로 애그리거트 수준에서 `지연 로딩` 을 하는 것과 동일한 결과를 만든다
  * 한 애그리거트에서 다른 애그리거트를 수정하는 문제를 근원적으로 방지할 수 있다
    * 외부 에그리거트를 직접 참조하지 않기 때문에 애초에 한 애그리거트에서 다른 애그리거트의 상태를 변경할 수 없는 것이다!

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

* 다른 애그리거트를 ID로 참조하면 참조하는 `여러 애그리거트`를 읽을 때 `조회 속도` 가 문제 될 수 있다
  * `N + 1 조회 문제`
    * 조회 대상이 N개일 때 N개를 읽어오는 한 번의 쿼리와 연관된 데이터를 읽어오는 쿼리를 N번 실행한다는 뜻
    * ID를 이용한 애그리거트 참조는 `지연 로딩` 과 같은 효과를 만드는데, 지연 로딩과 관련된 대표적인 문제가 `N + 1 조회 문제` 이다
    * 이 문제가 발생하지 않도록 하려면 `JOIN` 을 사용해야 한다
* `ID 참조 방식` 을 사용하면서 `N+1` 조회와 같은 문제가 발생하지 않도록 하려면 `조회 전용 쿼리` 를 사용하면 된다
  * 데이터 조회를 위한 별도 DAO를 만들고, DAO의 조회 method에서 `JOIN` 을 이용해 한 번의 쿼리로 필요한 데이터를 로딩하면 된다
  * `JPQL`을 이용해서 각각의 애그리거트를 `JOIN` 으로 조회하여 쿼리로 로딩할 수 있다
    * 즉시 로딩이나 지연 로딩과 같은 로딩 전략을 고민할 필요 없이 조회 화면에서 필요한 애그리거트 데이터를 한 번의 쿼리로 로딩할 수 있다
    * 복잡한 쿼리 or SQL 에 특화된 기능을 사용해야 한다면, 조회를 위한 부분만 `MyBatis` 와 같은 기술을 이용해서 구현할 수도 있다
* 애그리거트마다 다른 저장소를 사용하면 한 번의 쿼리로 관련 애그리거트를 조회할 수 없는데, 이때는 `조회 성능` 을 높이기 위해 `캐시` 를 적용하거나 `조회 전용 저장소` 를 따로 구성할 수 있다
  * 단점: 코드가 복잡해짐
  * 장점: 시스템의 처리량을 높일 수 있음

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

* `1-N`과 `M-N` 연관은 `컬렉션` 을 이용한 연관이다
  * ex) 카테고리 - 상품 간 연관 (1-N)
* 애그리거트 간 `1-N` 관계는 `Set` 과 같은 컬렉션을 사용해서 표현할 수 있다
  * But, 개념적으로 존재하느 애그리거트 간의 `1-N` 연관을 실제 구현에 반영하는 것이 요구사항을 충족하는 것과는 상관없을 때가 있다
    * 보통 목록 관련 요구사항은 페이징을 이용해 나눠서 보여주는데, 개수가 많다면 코드를 실행할 때마다 실행 속도가 급격히 느려져 성능에 문제를 일으킨다
    * 개념적으로는 애그리거트 간에 `1-N` 연관이 있더라도, 이런 성능 문제 때문에 애그리거트 간의 `1-N` 연관을 실제 구현에 반영하지 않는다
* 카테고리에 속한 상품을 구할 필요가 있다면, 상품 입장에서 자신이 속한 카테고리를 `N-1` 로 연관 지어 구하면 된다
* `M-N` 연관은 개념적으로 양쪽 애그리거트에 컬렉션으로 연관을 만든다
  * 실제 요구사항을 고려하여 `M-N` 연관을 구현에 포함시킬지를 결정해야 한다
    * 개념적으로는 상품과 카테고리가 양방향 `M-N` 연관이 존재하지만, 실제 구현에서는 상품 → 카테고리로의 단방향 `M-N` 연관만 적용하면 되는 것이다

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

* 애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면, 애그리거트에 `팩토리 method` 를 구현하는 것을 고려해보자
* ex)
  * Product의 경우 제품을 생성한 Store의 식별자를 필요로 한다
    * 즉, Store의 데이터를 이용해서 Product를 생성한다
    * 게다가 Product를 생성할 수 있는 조건을 판단할 때 Store의 상태를 이용한다
  * 따라서 Store에 Product를 생성하는 `팩토리 method` 를 추가하면 Product를 생성할 때 필요한 데이터의 일부를 `직접 제공` 하면서 동시에 `중요한 도메인 로직` 을 함께 구현할 수 있게 된다
