Chapter 3: 애그리거트
💡 책에서 기억하고 싶은 내용
애그리거트
상위 수준
에서 전체 모델을 정리하면 도메인 모델의 복잡한 관계를 이해하는 데 도움이 된다개별 객체 수준에서 모델을 바라보면 상위 수준에서 관계를 파악하기 어렵다
복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만들려면 상위 수준에서 모델을 조망할 수 있는 방법이 필요한데, 그 방법이 바로
애그리거트
다!애그리거트는
복잡한 모델
을관리하는 기준
을 제공한다일관성
을 관리하는 기준도 된다
애그리거트는 관련된 모델을 하나로 모았기 때문에, 한 애그리거트에 속한 객체는
유사하거나 동일한 라이프 사이클
을 갖는다애그리거트에 속한 구성요소는 대부분 함께 생성하고 함께 제거한다
애그리거트는
경계
를 갖는다한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다
애그리거트는
독립된 객체 군
이며, 각 애그리거트는자신을 관리
할 뿐 다른 애그리거트를 관리하지 않는다
경계
를 설정할 때 기본이 되는 것은도메인 규칙
과요구사항
이다도메인 규칙에 따라
함께 생성/변경되는 구성요소
는 한 애그리거트에 속할 가능성이 높다흔히 ‘A가 B를 갖는다' 로 설계할 수 있는 요구사항이 있다면 A와 B를 한 애그리거트로 묶어서 생각하기 쉽지만, 반드시 A와 B가 한 애그리거트에 속하는 것은 아니다!
다수의 애그리거트가 한 개의 엔티티 객체만 갖는 경우가 많고, 두 개 이상의 엔티티로 구성되는 애그리거트는 드물다… 는 저자의 경험담
애그리거트 루트
애그리거트는 여러 객체로 구성되기 때문에 한 객체만 상태가 정상이면 안 된다!
도메인 규칙을 지키려면 애그리거트에 속한 모든 객체가 정상 상태를 가져야 한다
애그리거트에 속한 모든 객체가
일관된 상태
를 유지하려면 애그리거트 전체를 관리할 주체가 필요한데, 이 책임을 지는 것이 애그리거트의루트 엔티티
다루트 엔티티는 애그리거트의
대표 엔티티
다애그리거트에 속한 객체는 루트 엔티티에
직접 or 간접적으로 속하게 된다
애그리거트 루트는 도메인 규칙을 구현한 기능을 제공한다
도메인 규칙과 일관성
애그리거트의 핵심 역할은 애그리거트의
일관성
이 깨지지 않도록 하는 것이다이를 위해 애그리거트 루트는 애그리거트가 제공해야 할
도메인 기능을 구현
한다
애그리거트 루트가 제공하는 method는
도메인 규칙
에 따라 애그리거트에 속한 객체의일관성
이 깨지지 않도록 구현해야 한다애그리거트 외부에서 애그리거트가 속한 객체를 직접 변경하면 안 된다
이렇게 하면 애그리거트 루트가
강제하는 규칙
을 적용할 수 없어 모델의 일관성을 깨게 된다
불필요한 중복을 피하고 애그리거트 루트를 통해서만 도메인 로직을 구현하게 만들기 위해 도메인 모델에 적용해야 하는 것
단순히 field를 변경하는
set method
를public
으로 만들지 않는다공개 set method는 도메인의 의미나 의도를 표현하지 못하고, 도메인 로직을 도메인 객체가 아닌 응용 영역 or 표현 영역으로 분산시킨다
공개 set method를 사용하지 않의면 의미가 드러나는 method를 사용해서 구현할 가능성이 높아진다
ex) cancel(), changePassword()
밸류 타입
은불변
으로 구현한다밸류 객체의 값을 변경할 수 없으면 애그리거트 루트에서 밸류 객체를 구해도, 애그리거트 외부에서 밸류 객체의 상태를 변경할 수 없다
밸류 객체가 불변일 때 밸류 객체의 값을 변경하는 방법은 새로운 밸류 객체를 할당하는 것뿐이다!
밸류 타입
의내부 상태
를 변경하려면애그리거트 루트
를 통해서만 가능하므로, 애그리거트 루트가 도메인 규칙을 올바르게 구현하면 애그리거트 전체의일관성
을 올바르게 유지할 수 있다
애그리거트 루트의 기능 구현
애그리거트는 구성요소의 상태를 참조하는 것 뿐만 아니라,
기능 실행
을 위임하기도 한다보통 한 애그리거트에 속하는 모델은 한 패키지에 속하기 때문에
package
나protected
범위를 사용하면 애그리거트 외부에서 상태 변경 기능을 실행하는 것을 방지할 수 있다
트랜잭션 범위
트랜잭션 범위는
작을수록
좋다잠금 대상이 많아진다는 것은 그만큼 동시에 처리할 수 있는 트랜잭션 수가 줄어든다는 것을 의미하고, 전체적인 성능(처리량)을 떨어뜨린다
한 트랜잭션
에서는한 개의 애그리거트만
수정해야 한다한 트랜잭션에서 두 개 이상의 애그리거트를 수정하면
트랜잭션 충돌
이 발생할 가능성이 더 높아지기 때문에, 수정하는 애그리거트 개수가 많아질수록 전체 처리량이 떨어지게 된다
한 트랜잭션에서 한 애그리거트만 수정하는 것은 애그리거트에서
다른 애그리거트를 변경하지 않는다
는 것을 의미한다다른 애그리거트를 수정하면 결과적으로 두 개의 애그리거트를 한 트랜잭션에서 수정하게 된다
한 애그리거트가 다른 애그리거트의 기능에 의존하기 시작하면 애그리거트 간
결합도
가 높아진다
만약 부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트를 수정해야 한다면, 애그리거트에서 다른 애그리거트를 직접 수정하지 말고
응용 서비스
에서 두 애그리거트를 수정하도록 구현한다
리포지터리와 애그리거트
애그리거트는 개념상
한 개의 도메인 모델
을 표현하므로 객체의영속성
을 처리하는 리포지터리는애그리거트 단위
로 존재한다애그리거트는 개념적으로 하나이므로 리포지터리는
애그리거트 전체
를 저장소에영속화
해야 한다ex) Order 애그리거트와 관련된 테이블이 3개면?
Order 애그리거트 저장 시 애그리거트 루트와 매핑되는 테이블 + 애그리거트에 속한 모든 구성요소에 매핑된 테이블에 데이터를 저장해야 한다
애그리거트를
영속화
할 저장소로 무엇을 사용하든지 간에 애그리거트의 상태가 변경되면 모든 변경을원자적
으로 저장소에 반영해야 한다애그리거트에서 두 개의 객체를 변경했는데, 저장소에는 한 객체에 대한 변경만 반영되면
데이터 일관성
이 깨지므로 문제가 된다!
ID를 이용한 애그리거트 참조
애그리거트 관리 주체는
애그리거트 루트
이므로 애그리거트에서 다른 애그리거트를 참조한다는 것은다른 애그리거트의 루트를 참조
한다는 것과 같다애그리거트 간의 참조는
field
를 통해 구현할 수 있다Field
를 이용한 애그리거트 참조는 개발자에게 구현의 편리함을 제공하지만, 아래와 같은 문제를 야기할 수 있다편한 탐색 오용
한 애그리거트 내부에서 다른 애그리거트 객체에 접근할 수 있으면 다른 애그리거트의 상태를 쉽게 변경할 수 있게 된다
한 애그리거트에서 다른 애그리거트의 상태를 변경하는 것은 애그리거트 간의
의존 결합도
를 높여서, 결과적으로 애그리거트의 변경을 어렵게 만든다
성능에 대한 고민
애그리거트를 직접 참조하면, 성능과 관련된 여러 고민을 해야 한다
JPA를 사용하면 참조한 객체를
지연(lazy) 로딩
과즉시(eager) 로딩
의 방식으로 로딩할 수 있는데, 둘 중 무엇을 사용할지는 애그리거트의 어떤 기능을 사용하느냐에 따라 달라진다단순히 연관된 깨체의 데이터를 함께 화면에 보여줘야 하면,
즉시 로딩
이 조회 성능에 유리하지만,애그리거트의 상태를 변경하는 기능을 실행하는 경우에는 불필요한 객체를 함께 로딩할 필요가 없으므로
지연 로딩
이 유리할 수 있다
다양한 경우의 수를 고려해서
연관 매핑
과JPQL/Criteria
쿼리의 로딩 전략을 결정해야 한다
확장 어려움
사용자가 늘고 트래픽이 증가하면
부하를 분산
하기 위해 하위 도메인별로 시스템을 분리하기 시작하는데, 이 과정에서 서로 다른 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를 생성할 때 필요한 데이터의 일부를직접 제공
하면서 동시에중요한 도메인 로직
을 함께 구현할 수 있게 된다
Last updated