3장: 애그리거트

애그리거트

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

  • 백 개 이상의 테이블을 한 ERD에 표현하면 개별 테이블 간 관계 파악에 치우쳐 큰 틀에서 이해하기 어려운 것처럼, 도메인 객체 모델이 복잡해지면 전반적인 구조나 큰 틀에서 도메인 간의 관계를 파악하기 어려워진다.

  • 주요 도메인 요소 간의 관계를 파악하기 어려우면 코드를 변경하고 확장하는 것이 어려워진다.

  • 애그리거트 단위를 나타내기 위해 관련된 객체를 하나의 군집으로 묶으면, 상위 수준에서 도메인 모델의 관계를 파악할 수 있도록 한다.

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

  • 한 애그리거트에 속한 객체들은 대부분 유사하거나 동일한 라이프 사이클을 갖는다. 아래와 같이 주문 애그리거트를 만드려면 내부에 존재하는 모든 객체가 생성된 상태여야 한다.

  • 애그리거트는 경계를 가지며, 이를 설정할 때 기본이 되는 것은 도메인 규칙과 요구사항이다. 도메인 규칙에 따라 함께 생성되는 구성요소는 한 애그리거트에 속할 가능성이 높다.

  • ‘A가 B를 갖는다’로 설계할 수 있는 요구사항이 있을 때, A와 B가 반드시 한 애그리거트에 속한다는 것을 의미하진 않는다. 예를 들어 상품은 다수의 리뷰를 갖지만, 상품 엔티티와 리뷰 엔티티는 함께 생성/변경되지 않으며 변경 주체도 다르다. 따라서 이 둘은 서로 다른 애그리거트에 속한다.

  • 처음 도메인 모델을 만들기 시작하면 큰 애그리거트로 보이는 것들이 많지만, 도메인에 대한 경험이 생기고 도메인 규칙을 제대로 이해할수록 애그리거트의 실제 크기는 줄어든다.

애그리거트 루트

도메인 규칙과 일관성

  • 애그리거트는 여러 객체로 구성되며 도메인 규칙을 지키려면 애그리거트에 속한 모든 객체가 정상 상태를 가져야 한다.

  • 루트 엔티티는 애그리거트에 속한 모든 객체가 일관된 상태를 유지하기 위해 애그리거트 전체를 관리하는 주체이다.

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

  • 애그리거트 루트는 메서드를 제공하여 도메인 규칙에 따라 애그리거트에 속한 객체의 일관성이 깨지지 않도록 한다.

    • 예를 들어 주문 엔티티에 존재하는 배송지 정보 변경 메서드는 배송 시작 전에만 변경이 가능하다는 도메인 규칙이 충족할 때에만 정보를 변경하도록 구현해야 한다.

  • 애그리거트 외부에서 애그리거트에 속한 객체를 직접 변경하면 안 된다. 이렇게 구현할 경우 애그리거트 루트가 도메인 규칙을 적용할 수 없게 되어 모델의 일관성이 깨지게 된다.

  • 이를 막기 위해서는 단순히 필드를 변경하는 setter를 제공하지 말아야 하며, 밸류 타입을 불변으로 구현해야 한다.

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

  • 애그리거트 루트는 애그리거트 내부의 다른 객체를 조합해서 기능을 완성한다. 구성요소의 상태를 참조하기도 하고 기능 실행을 위임하기도 한다.

  • 아래는 회원 루트 엔티티에서 비밀번호 엔티티에 기존 암호가 일치하는지 확인하고 새로운 비밀번호로 변경하는 메서드의 구현이다.

트랜잭션 범위

  • 트랜잭션의 범위는 작을수록 좋다. 다른 메서드에서의 수정을 막기 위해 트랜잭션에서는 락을 걸어야 하는데, 트랜잭션 내부에서 여러 테이블에 관여하게 되면 락을 거는 엔티티들이 많아지게 된다. 이로 인해 동시에 처리할 수 있는 트랜잭션 개수가 줄어들면 전체적인 성능이 떨어지게 된다.

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

  • 예를 들어 하나의 메서드에서 배송지 정보를 변경하면서 동시에 배송지 정보를 회원의 주소로 설정하도록 하는 것은 주문 애그리거트에서 회원 애그리거트의 상태를 변경하는 것이기 때문에 좋지 않은 방법이다.

  • 애그리거트는 최대한 서로 독립적이어야 하며 한 애그리거트가 다른 애그리거트의 기능에 의존하기 시작하면 애그리거트 간 결합도가 높아져 코드 수정이 어려워진다.

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

  • 아래 두 코드 블록을 보면, Order 애그리거트에서 Member 애그리거트를 직접 접근할 경우 결합도가 높아지기 때문에 ChangeOrderService라는 응용 서비스에서 각 애그리거트에 접근하는 방법으로 리팩토링한 것을 알 수 있다.

  • 팀의 표준에 따라 사용자 유스케이스와 관련된 응용 서비스의 기능을 한 트랜잭션으로 실행해야 하는 경우가 있거나 기술 제약, UI 구현의 편리를 위해서는 한 트랜잭션에서 두 개 이상의 애그리거트를 변경하는 것을 고려해볼 수 있다.

리포지토리와 애그리거트

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

  • Order와 OrderLine을 물리적으로 각각 별도의 DB 테이블에 저장할 수 있지만 이를 조회할 때에는 각각 리포지토리를 구현해 조회하는 것이 아니라 하나의 리포지토리를 통해 통합된 형태로 조회해야 한다.

  • 리포지터리가 완전한 애그리거트를 제공하지 않으면 필드나 값이 올바르지 않아 애그리거트의 기능을 실행하는 도중에 NullPointerException과 같은 문제가 발생할 수 있다.

  • 애그리거트를 영속화할 저장소로 무엇을 사용하든지 간에 애그리거트의 상태가 변경되면 모든 변경을 원자적으로 저장소에 반영해야 한다.

  • RDBMS를 사용하는 경우 트랜잭션을 이용해 변경이 모두 저장소에 반영되는 것을 보장할 수 있으며, 몽고 DB를 사용하는 경우 한 애그리거트를 한 개의 Document에 저장함으로써 모두 저장소에 반영되는 것을 보장할 수 있다.

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

  • 한 애그리거트는 다른 애그리거트를 참조할 수 있다.

  • 애그리거트 관리 주체는 애그리거트 루트이므로 애그리거트에서 다른 애그리거트를 참조할 때는 다른 애그리거트의 루트를 참조한다.

  • 아래와 같이 주문 애그리거트 내부의 주문자 객체에는 주문한 회원을 참조하기 위해 회원 애그리거트 루트인 Member를 필드로 두어 참조할 수 있다.

  • 필드를 이용한 애그리거트 참조는 편한 탐색 오용, 성능 문제, 확장의 어려움 문제를 발생시킬 수 있다.

    • 다른 애그리거트 객체에 접근하여 상태를 변경하는 불상사가 발생할 수 있다.

    • 애그리거트를 직접 참조할 경우 지연 로딩과 즉시 로딩 중 어떤 방식을 사용해야 할 지 고민해야 한다. 조회 목적이라면 즉시 로딩이 성능에 유리하고, 상태 변경이 목적이라면 지연 로딩이 유리할 수 있다.

  • 이러한 문제를 완화하기 위해 ID를 참조하는 방식을 사용한다. 이를 통해 애그리거트 간의 의존을 제거하여 응집도를 높이는 효과를 낸다.

  • ID를 참조하게 되면 해당 애그리거트 데이터가 필요한 경우에 응용 서비스에서 ID를 이용해 데이터를 로딩해오면 되므로 지연 로딩, 즉시 로딩에 대한 고민을 할 필요가 없어진다.

  • 애그리거트 별로 다른 구현 기술을 사용하는 것도 가능해진다.

문제점

  • 다른 애그리거트를 ID로 참조하면 참조하는 여러 애그리거트를 읽을 때 조회 속도가 문제 될 수 있다.

  • 조인을 통해 데이터를 한 번에 가져올 수 없기 때문에, N+1 조회 문제가 발생하게 된다. 이를 막기 위해서는 조회 전용 쿼리를 레포지토리 내부에 두어 한 번의 쿼리로 로딩할 수 있도록 하면 된다. 다만 이 방식은 애그리거트마다 서로 다른 저장소를 사용할 경우 불가능하며, 캐시나 조회 전용 저장소를 따로 구성해야 한다.

애그리거트 간 집합 연관

  • 애그리거트 간 1-N과 M-N 연관이 있다면 이를 보통 컬렉션을 이용해 표현할 것이다.

  • 하지만 개념적으로 애그리거트 간에 1-N 연관이 있더라도, 성능 문제 때문에 애그리거트 간의 1-N 연관을 실제 구현에는 반영하지 않는다.

  • 아래는 1-N을 위해 카테고리에 속하는 모든 상품을 리스트로 넣어두었지만, 상품의 개수가 무수히 많아 페이징 방식으로 상품을 조회하는 아주 비효율적인 코드의 예시이다.

  • 위와 같이 컬렉션을 두는 대신, Product (N에 해당하는 애그리거트)쪽에 category id를 넣고 Product의 레포지토리에서 카테고리 별 상품을 조회하면 효율적인 코드를 작성할 수 있다.

  • M-N 연관은 개념적으로 양쪽 애그리거트에 컬렉션으로 연관을 만든다. 하나의 상품이 여러 카테고리에 속할 수 있고, 한 카테고리에는 여러 상품이 속하는 경우가 이에 해당한다.

  • M-N 요구사항을 실제 구현할 때는 보통 단방향 연관만 적용한다. 상품에서 카테고리로의 단방향 M-N 연관만 적용하면 되는 것이다.

  • RDBMS를 이용해서 M-N 연관을 구현하려면 조인 테이블을 사용한다.

  • JPA의 컬렉션 매핑을 이용해 상품이 속하는 카테고리를 컬렉션으로 유지할 수 있다. 그리고 JPQL의 member of 연산자를 이용해 특정 카테고리에 속하는 상품들을 조회할 수 있다.

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

  • 애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면 애그리거트에 팩토리 메서드를 구현하는 것을 고려해야 한다.

  • 상점이 상품을 등록하려 할 때 상점이 상품을 올릴 수 있는 상태인지 검증하는 과정을 응용 서비스 영역대신 애그리거트 내부에서 수행하면 도메인 기능을 캡슐화할 수 있다.

  • 즉, 상품을 생성하는 팩토리 역할을 상점 애그리거트에서 구현하고 응용 서비스는 이를 호출하기만 하면 된다. 이를 통해 상품 생성 가능 여부 확인의 코드를 변경하고 싶을 때에는 도메인 코드만 변경하면 된다.

  • 만약 다른 애그리거트 생성 시 많은 정보를 알아야 한다면 도메인 코드 대신 독립된 팩토리에 생성을 위임할 수도 있다.

Last updated