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