Enjoy My Posts

DDD START 정리

Posted on By Geunwon Lim

CHAPTER 1 도메인 모델 시작

1. 도메인

개발자 입장에서 온라인 서점은 구현해야 할 소프트웨어 대상이다. 온라인 서점 소프트웨어는 온라인으로 책을 판매하는 데 필요한 상품조회, 구매, 결제, 배송추적등의 기능을 제공해야 한다. ‘온라인 서점’은 소프트웨어로 해결하고자 하는 문제영역, 즉 도메인이다. 온라인 서점은 조금 더 세부적인 문제영역인 하위 도메인으로 나눌 수 있다. 온라인 서점은 회원, 혜택, 주문, 결제, 배송, 정산, 카탈로그, 리뷰 등의 하위 도메인으로 나눌 수 있다. 하위 도메인을 어떻게 구성할지 여부는 상황에 따라 달라진다. 기업 고객을 대상으로 대형 장비를 판매한다고 할 때는 온라인으로 카탈로그를 제공하고, 주문서를 받는 정도만 필요할 것이다. 즉 온라인 결제나 배송추적과 같은 기능은 필요 없다. 반면에 의류나 액세서리처럼 일반 고객을 대상으로 물건을 판매한다면 카탈로그, 리뷰, 주문, 결제, 배상, 회원 기능 등이 필요할 것이다.

2. 도메인 모델

도메인 모델에는 다양한 정의가 존재한다. 이 책에서는 특정 도메인을 개념적으로 표현한 것이라고 정의한다(오브젝트의 경우 도메인을 이해할 수 있는 모든 것 이라고 정의했음). 도메인을 이해하려면 도메인이 제공하는 기능과 도메인의 주요 데이터 구성을 파악해야하는데, 이런 면에서 기능과 데이터를 함께 보여주는 객체 모델(UML)은 도메인을 모델링하기 위한 좋은 수단이다. UML 말고 딴것들도 다 도메인 모델이라고 할 수 있다. 도메인을 이해하는 데 도움이 된다면 표현 방식이 무엇인지는 중요하지 않다. 도출한 모델은 엔티티와 밸류로 구분할 수 있다.

CHAPTER 2 아키텍처 개요

1. 네 개의 영역

구분 설명
UI 사용자의 요청을 처리하고 사용자에게 정보를 보여준다. 스프링 mvc(컨트롤러)는 표현 형역에 해당하는 기술이다. 웹 어플리케이션에서 표현 영역은 http 요청을 응용 영역이 필요로 하는 형식으로 변환해서 응용 영역에 전달하고, 응용 영역의 응답을 HTTP 응답으로 변환해서 전송한다.
응용 사용자가 요청한 기능을 실행한다. 표현 영역을 통해 사용자의 요청을 전달받는 응용 영역은 시스템이 사용자에게 제공해야 할 기능을 구현한다. 예를 들어, ‘주문등록’, ‘주문취소’ 등을 구현한다. 응용 영역은 기능을 구현하기 위해 도메인 영역의 도메인 모델을 사용한다. 로직을 직접 수행(구현)하기보다는 도메인 모델에 로직 수행을 위임한다.
도메인 도메인 모델을 구현한다. 실제 로직이 위치한다. 즉 시스템이 제공할 도메인의 규칙(로직)을 구현한다. 핵심 규칙을 구현한 코드는 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다.
인프라스트럭처 인프라스트럭처 영역은 데이터베이스나 메시징 시스템과 같은 외부 시스템과의 연동 처리를 담당한다. 인프라스트럭처 영역은 구현 기술에 대한 것을 다룬다. rdbms 연동, 메시징 큐에 메시지를 전송하거나 수신하는 기능 구현 등, smtp를 이용한 메일 발송 기능, http 클라이언트를 이용해서 rest api를 호출하는 것 등을 처리한다. 논리적인 개념을 표현하기보다는 실제 구현을 다룬다. (도메인 영역이나 응용 영역은 추상화되어있는데, 여기는 그걸 구체화하는 기술을 다룸). 도메인 영역, 응용 영역, 표현 영역은 구현 기술을 사용한 코드를 직접 만들지 않는다. 대신 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다.

네 영역을 구성할 때 흔히들 표현 - 응용 - 도메인 - 인프라스터럭처 구조를 취한다. 계층 구조는 그 특성상 상위 계층에서 하위 계층으로의 의존만 존재하고 하위 계층은 상위 계층에 의존하지 않는다. 응용, 도메인 영역이 로직을 수행할 때 db나 외부 시스템 연동을 위해 인프라스트럭처의 기능을 사용하므로 이런 계층 구조가 자연스러워 보인다. 하지만 짚고 넘어가야 할 것인데 바로 표현, 응용, 도메인 계층이 상세한 구현 기술을 다루는 인프라스트럭처 계층에 종속된다는 것이다. 이렇게 되면 두 가지 문제가 있다. 첫 번째 문제는 도메인 객체만 테스트하기 어렵다는 것이다. 두번째 문제는 구현 방식을 바꾸기 어렵다는 것이다. 예를들어 mysql을 postgresql로 바꾸려면 도메인도 바꿔야 할 수도 있다. 즉 인프라스터럭처에 의존하면 ‘테스트의 어려움’과 ‘기능 확장의 어려움’이 생긴다. 이 문제를 해결하려면 제어의 역전을 적용하면 된다(구두 설명 예정).

2. 도메인 영역의 주요 구성 요소

구분 설명
엔티티 엔티티의 가장 큰 특징은 식별자를 갖는다는 것이다. 두 엔티티 객체의 식별자가 같으면 두 엔티티는 같다고 판단할 수 있다. 필자는 신입 시절 도메인 모델을 만들 때 DB 테이블의 엔티티와 도메인 모델의 엔티티를 동일하게 생각했다고 하고, 경력이 쌓이며 다르다고 생각했다고 한다. 두 모델의 가장 큰 차이는 도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 함께 제공한다는 점이다. 도메인 모델의 엔티티는 단순히 데이터를 담고 있는 데이터 구조라기보다는 데이터와 함께 기능을 제공하는 객체이다. 또 다른 차이는 도메인 모델의 엔티티는 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해서 표현할 수 있다는 것이다.
밸류 개념적으로 완전한 하나를 표현할 때 사용한다. 고유의 식별자를 갖지 않는 객체로 주로 개념적으로 하나의 도메인 객체의 속성을 표현할 때 사용된다. 밸류 타입을 통해 개념을 명확하게 표현할 수 있다. 또한 밸류 타입을 위한 기능을 추가할 수 있다. 밸류 타입을 불변으로 구현하는 이유는 여러가지가 있지만 가장 중요한 이유는 불변 타입을 사용하면 보다 안전한 코드를 작성할 수 있다는 것이다.
애그리거트 애그리거트: 관련된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것이다. 예를 들어, 주문과 관련된 Order 엔티티, OrderLine 밸류, Orderer 밸류를 ‘주문’애그리거트로 묶을 수 있다. 도메인이 커질수록 개발할 도메인 모델도 커지면서 많은 엔티티와 밸류가 출현한다. 엔티티와 밸류 개수가 많아지면 많아질수록 모델은 점점 더 복잡해진다. 도메인 모델이 복잡해지면 개발자가 전체 구조가 아닌 한 개 엔티티와 밸류에만 집중하게 되는 경우가 발생한다. 지도를 볼 때 매우 상세하게 나온 걸 보면 내가 어디에 위치하고 있는지 이해하기 어려우므로, 큰 수준에서 멀찌감치 보여주는 소추적 지도를 함께 봐야 현재 위치를 보다 정확하게 이해할 수 있다. 이와 비슷하게 도메인 모델도 개별 객체뿐만 아니라 상위 수준에서 모델을 볼 수 있어야 저넻 모델의 관계와 개별 모델을 이해하는 데 도움이 된다. 도메인 모델에서 전체 구조를 이해하는 데 도움이 되는 것이 바로 애그리거트다. 애그리거트의 예시로 주문을 얘기해보자면, 주문이라는 도메인 개념은 ‘주문’, ‘배송지 정보’, ‘주문자’, ‘주문 목록’, ‘총결제 금액’ 등의 하위 모델(객체)로 구성되는데 이것들을 묶어서 ‘주문’이라고 뭉뚱그려 표현할 수 있다. 애그리거트를 사용하면 개별 객체가 아닌 관련 객체를 묶어서 객체 군집 단위로 모델을 바라볼 수 있게 된다. 개별 객체 간의 관계가 아닌 애그리거트 간의 관계로 도메인 모델을 이해하고 구현할 수 있게 되며, 이를 통해 큰 틀에서 도메인 모델을 관리할 수 있게 된다.
리포지터리 도메인 모델의 영속성을 처리한다. 리포지토리가 대상을 찾고 저장하는 단위는 애그리거트 루트이다.
도메인 서비스 특정 엔티티에 속하지 않은 도메인 로직을 제공한다. 예를 들어 ‘할인 금액 계산’은 상품, 쿠폰, 회원 등급, 구매 금액 등 다양한 조건을 이용해서 구현하는데, 이렇게 도메인 로직이 여러 엔티티와 밸류를 필요로 할 경우 도메인 서비스에서 로직을 구현한다.

참고 - dto

오래 전에 사용했던 프레임워크는 요청 파라미터나 DB 칼럼의 값을 설정할 때 set 메서드를 필요로 했기 때문에 구현 기술을 적용하려면 어쩔 수 없이 DTO에 get/set 메서드를 구현해야 했다. DTO가 도메인 로직을 담고 있지는 않기에 get/set 메서드를 제공해도 도메인 객체의 데이터 일관성에 영향을 줄 가능성은 낮다. 최근의 개발 프레임워크나 개발 도구는 set 메서드가 아닌 private 필드에 직접 값을 할당할 수 있는 기능을 제공하고 있다. 따라서 set 메서드를 제공하지 않아도 프레임워크를 이용해서 데이터를 전달받을 수 있다. 프레임워크가 필드에 직접 값을 할당하는 기능을 제공하고 있다면 set 메서드를 만드는 대신 해당 기능을 최대한 활용하자. 이렇게 하면 DTO도 불변 객체가 되어 불변의 장점을 DTO까지 확장할 수 있게 된다.

CHAPTER 3 애그리거트

객체 단위의 다이어그램을 보는 것보다 상위 수준에서 정리한 모델을 보는 것이 도메인을 더 쉽게 이해하는 데 도움이 된다. 수백개의 클래스를 보고 단번에 도메인을 이해하기는 어려울 것이다. 복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만들려면 상위 수준에서 모델을 조망할 수 있는 방법이 필요한데, 그 방법이 바로 애그리거트다. 수많은 객체를 애그리거트로 묶어서 바라보면 좀 더 상위 수준에서 도메인 모델 간의 관계를 파악할 수 있다. 애그리거트는 모델을 이해하는 데 도움을 줄 뿐 아니라 일관성을 관리하는 기준이 된다.

애그리거트는 관련된 모델을 하나로 모은 것이기 때문에 한 애그리거트에 속한 객체는 유사하거나 동일한 라이프사이클을 갖는다. 예를 들어 주문 애그리거트를 만들려면 Order, OrderLine, Orderer와 같은 관련 객체를 함께 생성해야 한다. Order는 생성했는데 ShippingInfo는 만들지 않았거나 ShippingInfo는 생성하면서 Order를 생성하지 않는 경우는 없다. 예외 상황이 있긴 하지만 대부분의 경우 애그리거트에 속한 구성요소는 함께 생성하고 함께 제거한다.

애그리거트는 객체들의 경계라고 볼 수 있다. 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다. 애그리거트는 독립된 객체 군이며, 각 애그리거트는 자기 자신을 관리할 뿐 다른 애그리거트를 관리하지 않는다. 경계를 설정할 때 기본이 되는 것은 도메인 규칙(로직)과 요구사항이다. 도메인 규칙에 따라 함께 생성되는 구성요소는 한 애그리거트에 속할 가능성이 높다. 즉 함께 변경되는 빈도가 높은 객체는 한 애그리거트에 속할 가능성이 높다.

흔히 ‘A가 B를 갖는다’로 설계할 수 있는 요구사항이 있다면 A와 B를 한 애그리거트로 묶어서 생각하기 쉽다. 주문의 경우 Order가 ShippingInfo와 Orderer를 가지므로 어느 정도 타당해보인다. 하지만 ‘A가 B를 갖는다’로 해석할 수 있는 요구사항이 있다고 해서 꼭 A와 B가 한 애그리거트에 속한다는 것을 의미하지는 않는다. 좋은 예가 상품과 리뷰다. 상품 상세 페이지에 들어가면 상품 상세 정보와 함께 리뷰 내용을 보여줘야 한다는 요구사항이 있다면 Product 엔티티와 Review 엔티티가 한 애그리거트에 속한다고 생각할 수 있다. 하지만 Product와 Review는 함께 생성되지 않께 변경되지도 않는다. 게다가 Product를 변경하는 주체가 상품 담당자라면 Review를 생성하고 변경하는 주체는 고객이다. Review의 변경이 Product에 영향을 주지 않고 반대로 Product의 변경이 Review에 영향을 주지도 않는다. 즉 상품과 리뷰는 함께 생성되거나 변경되지 않고 변경 주체도 다르기 때문에 서로 다른 애그리거트에 속한다.

1. 애그리거트 루트

애그리거트는 여러 객체로 구성되기 때문에 한 객체만 상태가 정상이어서는 안 된다. 도메인 규칙을 지키려면 애그리거트에 속한 모든 객체가 정상 상태를 가져야 한다. 애그리거트에 속한 모든 객체가 일관된 상태를 유지하려면 애그리거트 전체를 관리할 주체가 필요한데 이 책임을 지는 것이 바로 애그리거트의 루트 엔티티다. 애그리거트 루트 엔티티는 애그리거트의 대표 엔티티로 애그리거트에 속한 객체는 애그리거트 루트 엔티티에 직접 또는 간접적으로 속한다(즉 관련이 있다).

도메인 규칙과 일관성

애그리거트 루트의 핵심 역할은 애그리거트의 일관성이 깨지지 않도록 하는 것이다. 이를 위해 애그리거트 루트는 애그리거트가 제공해야 할 도메인 기능을 구현한다. 예를 들어 주문 애그리거트는 배송지 변경, 상품 변경과 같은 기능을 제공하는데 애그리거트 루트인 Order객체가 이 기능을 구현한 메서드를 제공한다.

애그리거트 루트가 아닌 다른 객체가 애그리거트에 속한 객체를 직접 변경하면 안된다. 이는 애그리거트 루트가 강제하는 규칙을 적용할 수 없어 모델의 일관성을 깨는 원인이 된다. 애그리거트 루트를 통하지 않고, 다른 객체로 애그리거트에 속한 객체를 직접 변경하더라도 일관성을 지킬 순 있다. 일관성을 지키기 위해 애그리거트 루트가 가진 로직을 그 객체도 가지면 된다. 하지만 이렇게 되면 동일한 검사 로직을 여러 객체에 중복해서 구현할 가능성이 높아져 그로 인한 부작용이 생긴다. 불필요한 중복을 피하고 애그리거트 루트를 통해서만 도메인 로직을 구현하게 만들려면 도메인 모델에 대해 다음의 두 가지를 습관적으로 적용해야 한다.

  1. 단순히 필드를 변경하는 set 메서드를 공개(public) 범위로 만들지 않는다. : 공개 set 메서드는 중요 도메인의 의미나 의도를 표현하지 못하고 도메인 로직이 도메인 객체가 아닌 응용 영역이나 표현 영역으로 분산되게 만드는 원인이 된다. 도메인 로직이 한곳에 응집되어 있지 않게 되므로 코드를 유지보수할 때 분석하고 수정하는 데 많은 시간을 들이게 된다. set 메서드를 사용하지 않게 되면 의미가 드러나는 메서드를 사용해서 구현할 가능성이 높아진다.
  2. 밸류 타입은 불변으로 구현한다. : 밸류 객체의 값을 변경할 수 없으면 애그리거트 루트에서 밸류 객체를 구해도 값을 변경할 수 없기 때문에 애그리거트 외부에서 밸류 객체의 상태를 변경할 수 없게 된다. 애그리거트 외부에서 내부 상태를 함부로 바꾸지 못하므로 애그리거트의 일관성이 깨질 가능성이 줄어든다. 밸류 객체가 불변이면 밸류 객체의 값을 변경하는 방법은 새로운 밸류 객체를 할당하는 것뿐이다. 결론적으로 밸류 타입의 내부 상태를 변경하려면 애그리거트 루트를 통해서만 가능하다. 그러므로 애그리거트 루트가 도메인 규칙을 올바르게 구현하면 애그리거트 전체의 일관성을 올바르게 유지할 수 있다.

트랜잭션 범위

트랜잭션 범위는 작을수록 좋다. DB 테이블을 기준으로 한 트랜잭션이 한 개 테이블을 수정하는 것과 세 개의 테이블을 수정하는 것은 성능에서 차이가 발생한다. 한 개 테이블을 수정할 때는 트랜잭션 충돌을 막기 위해 잠그는 대상이 한 개 테이블의 한 행으로 한정되지만, 세 개의 테이블을 수정하면 잠금 대상이 더 많아진다. 잠금 대상이 많아진다는 것은 그만큼 동시에 처리할 수 있는 트랜잭션 개수가 줄어든다는 것을 뜻하고 이는 전체적인 성능을 떨어뜨린다.

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

한 트랜잭션에서 한 애그리거트만 수정한다는 것은 애그리거트에서 다른 애그리거트를 변경하지 않는다는 것을 뜻한다. 한 애그리거트에서 다른 애그리거트를 수정하면 결과적으로 두 개의 애그리거트를 한 트랜잭션에서 수정하게 되므로 한 애그리거트 내부에서 다른 애그리거트의 상태를 변경하는 기능을 실행하면 안 된다. 만약 한 애그리거트가 다른 애그리거트의 상태를 변경한다면 이는 애그리거트가 자신의 책임 범위를 넘어 다른 애그리거트의 상태까지 관리하는 꼴이 된다. 애그리거트는 서로 최대한 독립적이어야 하는데 한 애그리거트가 다른 애그리거트의 기능에 의존하기 시작하면 애그리거트가 결합도가 높아지기 때문이다. 결합도가 높아질수록 향후 수정 비용이 증가하므로 애그리거트에서 다른 애그리거트의 상태를 변경하지 말아야 한다. 만약 부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트를 수정해야 한다면 애그리거트에서 다른 애그리거트를 직접 수정하지 말고 응용 서비스에서 두 애그리거트를 수정하도록 구현해야 한다. 참고로 도메인 이벤트를 사용하면 한 트랜잭션에서 한 개의 애그리거트를 수정하면서도 동기나 비동기로 다른 애그리거트의 상태를 변경하는 코드를 작성할 수 있다.

2. 리포지토리와 애그리거트

애그리거트는 개념상 완전한 한 개의 도메인 모델을 표현하므로 객체의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재한다. 애그리거트는 개념적으로 하나이므로 리포지터리는 애그리거트 전체를 저장소에 영속화해야 한다. 예를 들어 Order 애그리거트와 관련된 테이블이 세 개라면 리포지터리를 통해서 Order 애그리거트를 저장할 때 애그리거트 루트와 매핑되는 테이블뿐만 아니라 애그리거트에 속한 모든 구성요소를 위한 테이블에 데이터를 저장해야 한다. 같은 맥락에서 조회하는 메서드도 완전한 애그리거트를 제공해야 한다. 예를 들어 Order 애그리거트를 조회했을 때 OrderLine, Orderer 등 모든 구성요소를 포함하고 있어야 한다. 리포지터리가 완전한 애그리거트를 제공하지 않으면, NullPointerException과 같은 문제가 발생하게 된다.

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

한 객체가 다른 객체를 참조하는 것처럼 애그리거트도 다른 애그리거트를 참조한다. 애그리거트의 관리 주체가 애그리거트 루트이므로 애그리거트에서 다른 애그리거트를 참조한다는 것은 애그리거트의 루트를 참조한다는 것과 같다.

애그리거트 간의 참조는 필드를 통해 쉽게 구현할 수 있다. ORM 기술 덕에 애그리거트 루트에 대한 참조를 쉽게 구현할 수 있고, 필드를 이용한 애그리거트 참조를 사용하면 드른 애그리거트의 데이터를 객체 탐색을 통해 조회할 수 있다. 하지만 필드를 이용한 애그리거트 참조는 다음의 문제를 야기할 수 있다.

  1. 편한 탐색 오용(편라함 오용) : 한 애그리거트 내부에서 다른 애그리거트 객체에 접근할 수 있으면 다른 애그리거트의 상태를 쉽게 변경할 수 있게 된다. 트랜잭션 범위에서 언급한 것처럼 한 애그리거트가 관리하는 범위는 자기 자신으로 한정해야 한다. 그런데 애그리거트 내부에서 다른 애그리거트 객체에 접근할 수 있으면 구현의 편리함 때문에 다른 애그리거트를 수정하고자 하는 유혹에 빠지기 쉽다.
  2. 성능에 대한 고민 : 애그리거트를 직접 참조하면 성능에 관련된 여러 가지 고민을 해야 한다. JPA를 사용할 경우 참조한 객체를 지연 로딩과 즉시 로딩 두 가지 방식으로 로딩할 수 있다. 두 로딩 방식 중 무엇을 사용할지 여부는 애그리거트의 어떤 기능을 사용하느냐에 따라 달라진다. 단순히 연관된 객체의 데이터를 함께 화면에 보여주어야 하면 즉시 로딩이 조회 성능에 유리하지만, 애그리거트의 상태를 변경하는 기능을 실행하는 경우에는 불필요한 객체를 함께 로딩할 필요가 없으므로 지연 로딩이 유리하다. 성능을 위해 이러한 다양한 경우를 고려해야 한다.
  3. 확장 어려움 : 초기에는 단일 서버에 단일 DBMS로 서비스를 제공할 수 있다. 문제는 사용자가 몰리기 시작하면서 발생한다. 사용자가 늘고 트래픽이 증가하면 자연스럽게 부하를 분산하기 위해 하위 도메인별로 시스템을 분리하기 시작한다. 이 과정에서 하위 도메인마다 서로 다른 DBMS를 사용할 가능성이 높아진다. 한 하위 도메인은 마리아DB를 사용하고 다른 하위 도메인은 몽고DB를 사용하눈 식으로 말이다. 이는 더 이상 다른 애그리거트 루트를 참조하기 위해 JPA와 같은 단일 기술을 사용할 수 없음을 의미한다.

위 세 문제를 개선하는 것이 ID를 이용해 다른 애그리거트를 참조하는 것이다. ID를 이용한 참조는 DB 테이블에서 외래키를 사용해 참조하는 것과 비슷하게 다른 애그리거트를 참조할 때 ID를 참조한다는 것이다(애그리거트 내의 엔티티를 참조할 때는 객체 레퍼런스로 참조한다).

ID 참조를 사용하면 모든 객체가 참조로 연결되지 않고 한 애그리거트에 속한 객체들만 참조로 연결된다. 이는 애그리거트의 경계를 명확히 하고 애그리거트 간 물리적인 연결을 제거하기 때문에 모델의 복잡도를 낮춰준다. 또한 애그리거트 간의 의존을 제거하므로 응집도를 높여주는 효과도 있다. 구현 복잡도도 낮아진다. 다른 애그리거트를 직접 참조하지 않으므로 애그리거트 간 참조를 지연 로딩으로 할지 즉시 로딩으로 할지 고민하지 않아도 된다. 참조하는 애그리거트가 필요하면 응용 서비스에서 아이디를 이용해 로딩하면 된다. 응용 서비스에서 필요한 애그리거트를 로딩하므로 애그리거트 수준에서 지연 로딩을 하는 것과 동일한 결과를 만든다. ID를 이용한 방식을 사용하면 복잡도를 낮추는 것과 함께 한 애그리거트에서 다른 애그리거트를 수정하는 문제를 원천적으로 방지할 수 있다. 외부 애그리거트를 직접 참조하지 않기 때문에 애초에 한 애그리거트에서 다른 애그리거트의 상태를 변경할 수 없는 것이다. 애그리거트별로 다른 구현 기술을 사용하는 것도 가능해진다. 중요한 데이터인 주문 애그리거트는 RDBMS에 저장하고 조회 성능이 중요한 상품 애그리거트는 NOSQL에 저장할 수 있다. 또한 각 도메인을 별도 프로세스로 서비스하도록 구현할 수도 있다.

4. ID를 이용한 참조와 조회 성능

다른 애그리거트를 ID로 참조하면 참조하는 여러 애그리거트를 읽어야 할 때 조회 속도가 문제될 수 있다. 예를 들어, 주문 목록을 보여줄 때 한 DBMS에 데이터가 있다면 조인을 이용해서 한 번에 모든 데이터를 가져올 수 있지만, ID 참조 시 주문마다 상품 정보를 읽어오는 쿼리를 실행하게 된다. ID를 이용한 애그리거트 참조는 지연 로딩과 같은 효과를 만드는데 지연 로딩과 관련된 대표적인 문제가 바로 이 문제(N+1 조회)이다. N+1 조회 문제는 더 많은 쿼리를 실행해서 전체 조회 속도가 느려지는 원인이 된다. 이 문제가 발생하지 않도록 하려면 조인을 사용하도록 해야 한다. 조인을 사용하는 가장 쉬운 방법은 ID 참조 방식을 객체 참조 방식으로 바꾸고 즉시 로딩을 사용하도록 매핑 설정을 바꾸는 것이다. 하지만 이렇게 하면 애그리거트 간 참조를 ID 참조에서 객체 참조로 되돌리게 될 것이다.

ID 참조 방식을 사용하면서 N+1 조회와 같은 문제가 발생하지 않도록 하려면 전용 조회 쿼리(JPQL등을 활용하여)를 사용하면 된다. 예를 들어 데이터 조회를 위한 별도 DAO를 만들고 DAO의 조회 메서드에서 한 번의 쿼리로 필요한 데이터를 로딩하면 된다. 즉시 로딩이나 지연 로딩과 같은 로딩 전략을 고민할 필요 없이 조회 화면에서 필요한 애그리거트 데이터를 한 번의 쿼리로 로딩할 수 있게 된다.