Chapter 3: 애그리거트

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

애그리거트

  • 상위 수준에서 전체 모델을 정리하면 도메인 모델의 복잡한 관계를 이해하는 데 도움이 된다

    • 개별 객체 수준에서 모델을 바라보면 상위 수준에서 관계를 파악하기 어렵다

  • 복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만들려면 상위 수준에서 모델을 조망할 수 있는 방법이 필요한데, 그 방법이 바로 애그리거트다!

    • 애그리거트는 복잡한 모델관리하는 기준 을 제공한다

    • 일관성을 관리하는 기준도 된다

  • 애그리거트는 관련된 모델을 하나로 모았기 때문에, 한 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다

    • 애그리거트에 속한 구성요소는 대부분 함께 생성하고 함께 제거한다

  • 애그리거트는 경계를 갖는다

    • 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다

    • 애그리거트는 독립된 객체 군이며, 각 애그리거트는 자신을 관리할 뿐 다른 애그리거트를 관리하지 않는다

  • 경계를 설정할 때 기본이 되는 것은 도메인 규칙요구사항이다

    • 도메인 규칙에 따라 함께 생성/변경되는 구성요소 는 한 애그리거트에 속할 가능성이 높다

    • 흔히 ‘A가 B를 갖는다' 로 설계할 수 있는 요구사항이 있다면 A와 B를 한 애그리거트로 묶어서 생각하기 쉽지만, 반드시 A와 B가 한 애그리거트에 속하는 것은 아니다!

  • 다수의 애그리거트가 한 개의 엔티티 객체만 갖는 경우가 많고, 두 개 이상의 엔티티로 구성되는 애그리거트는 드물다… 는 저자의 경험담

애그리거트 루트

  • 애그리거트는 여러 객체로 구성되기 때문에 한 객체만 상태가 정상이면 안 된다!

    • 도메인 규칙을 지키려면 애그리거트에 속한 모든 객체가 정상 상태를 가져야 한다

  • 애그리거트에 속한 모든 객체가 일관된 상태를 유지하려면 애그리거트 전체를 관리할 주체가 필요한데, 이 책임을 지는 것이 애그리거트의 루트 엔티티

    • 루트 엔티티는 애그리거트의 대표 엔티티

    • 애그리거트에 속한 객체는 루트 엔티티에 직접 or 간접적으로 속하게 된다

  • 애그리거트 루트는 도메인 규칙을 구현한 기능을 제공한다

도메인 규칙과 일관성

  • 애그리거트의 핵심 역할은 애그리거트의 일관성이 깨지지 않도록 하는 것이다

    • 이를 위해 애그리거트 루트는 애그리거트가 제공해야 할 도메인 기능을 구현한다

  • 애그리거트 루트가 제공하는 method는 도메인 규칙에 따라 애그리거트에 속한 객체의 일관성이 깨지지 않도록 구현해야 한다

  • 애그리거트 외부에서 애그리거트가 속한 객체를 직접 변경하면 안 된다

    • 이렇게 하면 애그리거트 루트가 강제하는 규칙 을 적용할 수 없어 모델의 일관성을 깨게 된다

  • 불필요한 중복을 피하고 애그리거트 루트를 통해서만 도메인 로직을 구현하게 만들기 위해 도메인 모델에 적용해야 하는 것

    1. 단순히 field를 변경하는 set methodpublic으로 만들지 않는다

      • 공개 set method는 도메인의 의미나 의도를 표현하지 못하고, 도메인 로직을 도메인 객체가 아닌 응용 영역 or 표현 영역으로 분산시킨다

      • 공개 set method를 사용하지 않의면 의미가 드러나는 method를 사용해서 구현할 가능성이 높아진다

        • ex) cancel(), changePassword()

    2. 밸류 타입불변으로 구현한다

      • 밸류 객체의 값을 변경할 수 없으면 애그리거트 루트에서 밸류 객체를 구해도, 애그리거트 외부에서 밸류 객체의 상태를 변경할 수 없다

      • 밸류 객체가 불변일 때 밸류 객체의 값을 변경하는 방법은 새로운 밸류 객체를 할당하는 것뿐이다!

        • 밸류 타입내부 상태를 변경하려면 애그리거트 루트를 통해서만 가능하므로, 애그리거트 루트가 도메인 규칙을 올바르게 구현하면 애그리거트 전체의 일관성을 올바르게 유지할 수 있다

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

  • 애그리거트는 구성요소의 상태를 참조하는 것 뿐만 아니라, 기능 실행을 위임하기도 한다

  • 보통 한 애그리거트에 속하는 모델은 한 패키지에 속하기 때문에 packageprotected 범위를 사용하면 애그리거트 외부에서 상태 변경 기능을 실행하는 것을 방지할 수 있다

트랜잭션 범위

  • 트랜잭션 범위는 작을수록 좋다

    • 잠금 대상이 많아진다는 것은 그만큼 동시에 처리할 수 있는 트랜잭션 수가 줄어든다는 것을 의미하고, 전체적인 성능(처리량)을 떨어뜨린다

  • 한 트랜잭션에서는 한 개의 애그리거트만 수정해야 한다

    • 한 트랜잭션에서 두 개 이상의 애그리거트를 수정하면 트랜잭션 충돌 이 발생할 가능성이 더 높아지기 때문에, 수정하는 애그리거트 개수가 많아질수록 전체 처리량이 떨어지게 된다

  • 한 트랜잭션에서 한 애그리거트만 수정하는 것은 애그리거트에서 다른 애그리거트를 변경하지 않는다는 것을 의미한다

    • 다른 애그리거트를 수정하면 결과적으로 두 개의 애그리거트를 한 트랜잭션에서 수정하게 된다

    • 한 애그리거트가 다른 애그리거트의 기능에 의존하기 시작하면 애그리거트 간 결합도가 높아진다

  • 만약 부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트를 수정해야 한다면, 애그리거트에서 다른 애그리거트를 직접 수정하지 말고 응용 서비스 에서 두 애그리거트를 수정하도록 구현한다

리포지터리와 애그리거트

  • 애그리거트는 개념상 한 개의 도메인 모델 을 표현하므로 객체의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재한다

  • 애그리거트는 개념적으로 하나이므로 리포지터리는 애그리거트 전체를 저장소에 영속화해야 한다

    • 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-NM-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를 생성할 때 필요한 데이터의 일부를 직접 제공 하면서 동시에 중요한 도메인 로직 을 함께 구현할 수 있게 된다

Last updated