공부하는 내용을 정리하는 목적으로 작성하고 있습니다. 잘못 작성된 내용을 지적해주시면 좀더깊이 공부해서 내용을 수정하겠습니다.
애그리거트
시스템을 설계할 때, 너무 세세하게 분리하기 시작하면 전체 구조가 잘 보이지 않는다.
- 예를들어, 지도를 볼 때 확대해서 보면 어떤 건물, 도로가 있는지는 잘보이지만 해당지역이 어떻게 구성되어있는지는 알아보기 힘들다.
- 따라서 최초 설계는 상위개념으로 확실히 분리되는것으로 모델들을 정리하는게 전체구조를 파악하기 좋다.
- 쇼핑몰 주문시스템은 아래와 같이 간단하게 나눌 수 있다.
상위수준으로 분리하여 전체적인 구조의 윤곽이 잡혔다면, 객체 수준으로 낮추어 좀 더 상세히 관계를 분리한다.
- 연관있는 객체들끼리는 직접적으로 연결된다.
- 만약 어떤 객체에 대해 관계를 맺는것이 애매모호하다면(
어디에 속해야하는지 잘 모르겠다면) 구현할 때도 고민할 수 있게 된다.
이 때는 관련있는 객체들을 하나로 묶어서 집합구조로 만드는게 이해하기 편한데, 이것을 애그리거트라고 한다.
- 위의 객체들간 관계에 애그리거트 개념을 추가하면 아래와 같이 구조를 갖게 될 수 있다.
- 예를들어, 주문 애그리거트가 생성되려면 주문, 주문자, 배송정보, 주문항목 등의 객체가 필요하다.
- 주문자가 없는 주문, 배송지가 없는 주문, 주문항목이 없는 주문은 존재할 수 없다.
- 관련있는 객체들이 모여있으므로, 한 애그리거트에 속한 객체들은 유사한 라이프사이클을 갖게 된다.
- 애그리거트가 다르다는 것은 직접적인 관련이 없는, 즉 독립적인 라이프사이클을 갖는다고 할 수 있으며, 이것은 애그리거트 간 경계를 갖게 된다고 할 수 있다.
- 경계는 도메인규칙과 요구사항에 따라 구분짓게 된다.
잘못된 애그리거트
- 상품과 상품리뷰라는 객체가 있을 때, 이 둘은 항상 붙어있으므로 같은 애그리거트에 속할 수 있다고 생각할 수 있다.
- 하지만 둘은 변경주체가 다르다. (상품=판매자, 리뷰=구매자)
- 구매자가 리뷰를 변경한다고해서 이것은 상품에 영향을 1도 줄수없다.
- 판매자가 상품정보를 변경한다고해서(
가격변경, 내용변경 등등) 리뷰에 영향을 끼치지 않는다.
- 대부분의 애그리거트는 1개의 엔티티와 엔티티의 속성을 표현할 수 있는 다수의 밸류들로 포함된다.
애그리거트 루트
- 애그리거트에 속한 객체들의 상태가 무결성을 보장하려면 외부에서 객체들에게 접근을 허용해선 안된다.
- 주문 객체(엔티티)를 통하지 않고 외부에서 직접 주문항목의 개수를 변경하거나 배송지 정보를 변경해버리면 주문 객체가 갖고있는 주문정보와 변경된 객체의 정보가 불일치하는 문제가 발생할 수 있다.
- 따라서 외부에서 애그리거트에 접근하려면 반드시 루트 애그리거트를 통해서만 접근을 허용해야 한다.
- 이것은 인터페이스(public method)로 구현될 수 있는데 이것들이 결국 애그리거트 루트의 도메인 기능이 된다.
- 외부에 접근을 허용하는 인터페이스는 반드시 명확한 이름으로 제공되야 한다. (
setter X)
- 외부에 접근을 허용하는 인터페이스는 반드시 명확한 이름으로 제공되야 한다. (
- 이것은 인터페이스(public method)로 구현될 수 있는데 이것들이 결국 애그리거트 루트의 도메인 기능이 된다.
- 애그리거트 루트를 통해서만 외부에 접근을 허용하려면 아래 2가지 방법론이 필요하다.
- 도메인 기능에 해당되는 메서드는 public으로 구현하고, 필드를 변경하는 메서드는 private setter로 구현한다.
- 밸류는 불변 객체로 구현한다.
- 밸류 객체를 불변으로 구현해야만 외부에서 애그리거트 내부의 값을 수정할 수 없어 애그리거트의 무결성을 보장된다.
트랜잭션
- 하나의 트랜잭션에서 다수의 테이블을 변경하게되면 락의 범위가 넓어져 성능상 이슈가 발생할 수 있다.
- 따라서 하나의 트랜잭션은 하나의 테이블만 수정하도록 범위를 최소화 할 필요가 있다.
- 한 애그리거트는 대부분 하나의 엔티티를 갖게되므로, 한 트랙잭션은 한 애그리거트만 수정하게 된다. (충돌가능성 줄어듦)
- 예를들어, 주문 애그리거트에서 회원이나 상품 애그리거트를 수정하면 안된다.
- 다른 애그리거트의 수정이 가능하다는것은 결국 두 애그리거트 간의 결합도를 높이게되고 이것은 수정이 어려워져 유지보수가 힘들어진다.
- 만약 한 트랜잭션에서 2개 이상의 애그리거트를 수정하고 싶다면, 도메인 레벨에서 수정하지 말고 한단계 위인 응용서비스 계층에서 각각 애그리거트를 수정하도록 한다.
리포지터리와 애그리거트
- 애그리거트는 완전한 하나의 도메인 모델을 표현하므로, 엔티티의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재한다.
- 주문과 주문항목을 별도의 테이블로 구성한다고해서 리포지터리를 둘로 나누지 않는다. (애그리거트 루트에만 의존한다.)
- 애그리거트를 영속화하고, 조회하기 위해서는 최소 2개의 메서드가 필요하다.
- save: 애그리거트의 영속화
- findById: 애그리거트 조회
- 애그리거트를 영속화할 때, 리포지터리는 반드시 애그리거트에 속한 모든 요소들을 저장해야한다.
... // order 객체 뿐 아니라 order에 종속된 orderLines, orderState, shippingInfo 등이 모두 저장되어야 한다. orderRepository.save(order);
- 애그리거트를 조회할 때, 리포지터리는 반드시 애그리거트에 속한 모든 요소들을 조회해야한다.
... // order 객체는 order 뿐 아니라 종속된 orderLines, orderState, shippingInfo 등이 모두 함께 조회되어야 한다. // 만약 하위 객체들이 제대로 조회되지 않는다면 NullPointerException이 발생하게 된다. Order order = orderRepository.findById(orderId);
ID를 이용한 애그리거트 찹조
- 객체 레퍼런스를 통해 다른 애그리거트의 엔티티를 참조하게 될 경우, 몇 가지 문제가 발생할 수 있다.
- 해당 객체(엔티티)가 수정될 수 있는 문제가 있다.
- 해당 엔티티가 수정될 경우, 의존하고 있는 애그리거트도 수정될 수 있으므로 확장이 어려워진다.
로딩방식에 따라(지연로딩/즉시 로딩) 성능이 좌우될 수 있으므로 요구사항에 따른 고민이 많이 필요해진다.
- 따라서 같은 애그리거트 내에서 참조는 객체 레퍼런스를 사용하더라도, 다른 애그리거트의 참조는 ID를 통한 참조로 구현한다.
... // 주문 애그리거트에서 회원(구매자) 애그리거트의 잘못된 참조 (배송지 주소 변경) // 주문과 회원 애그리거트가 orderRepository에 종속되므로 두 애그리거트는 서로 다른 DBMS를 사용하기 어려워진다. orderer.getMember().changeAddress(shippingInfo.getAddress());
... // 주문 애그리거트에서 회원(구매자) 애그리거트의 잘된 참조 (배송지 주소 변경) // 주문과 회원 애그리거트가 각각 다른 리포지터리를 사용하므로 향후 두 애그리거트는 서로 다른 DBMS를 사용할 수 있다. Member member = memberRepository.findById(orderer.getMemberId()); member.changeAddress(shippingInfo.getAddress());
애그리거트 팩토리 사용
- 한 애그리거트에서 다른 애그리거트를 생성해야 하는경우 직접생성하면 두 애그리거트 간 의존성이 높아진다.
- 판매자의 차단상태를 체크하여 상품생성을 결정짓는 기능이 있다고 가정할 때, 아래와 같은 문제가 발생할 수 있다.
- 판매자의 차단상태를 체크하는것(회원 애그리거트)과 판매자가 상품을 생성하는것(상품 애그리거트)은 서로 다른 도메인이므로 응용 서비스에서 처리하게 된다.
- 판매자의 상태체크를 하거나 새로운 상품을 생성하는 비즈니스 로직은 도메인의 기능인데 이것이 응용 서비스에 노출된다.
- 응용 서비스에서 직접 상품을 생성하게 되면(new) 서비스와 상품 객체 간 의존성이 높아져, 추후 확장에 어려움이 발생할 수 있다.
- 따라서 판매자라는 별도의 객체를 생성하고 두 기능을 판매자 객체가 처리하도록 한다.
- 이렇게되면 응용서비스에서 판매자의 도메인 기능이 노출되지 않으므로 확장에 용이해진다.
... public class Store extends Member { public Product createProduct(ProductId id, ...) { if (blocked()) { ... } return new Product(id, ...); } }
... public class RegisterProductServicve { public ProductId registerNewProduct(NewProductRequest request) { Strong acount = accountRepository.findById(request.getStoreId()); checkNull(); productId id = productRepository.nextId(); // RegisterProductServicve에서 new Product(...);를 하지 않으므로 결합도가 약해진다. Product product = acount.createProduct(id, ...); productRepository.save(product); } }
- 이렇게되면 응용서비스에서 판매자의 도메인 기능이 노출되지 않으므로 확장에 용이해진다.
'Development Study > Domain Driven Design' 카테고리의 다른 글
[DDD-Start] Ch07. Domain Service (0) | 2020.08.25 |
---|---|
[DDD-Start] Ch06. 응용서비스와 표현영역 (0) | 2020.08.20 |
[DDD-Start] Ch05. 리포지터리의 조회기능(JPA중심) (0) | 2020.08.19 |
[DDD-Start] Ch02. 아키텍처 개요 (0) | 2020.08.05 |
[DDD-Start] Ch01. 도메인 모델 시작 (0) | 2020.08.04 |