애그리거트?
도메인 객체 모델이 복잡해지면 개별 구성요소 위주로 모델을 이해하게 되고 전반적인 구조나 도메인 간의 관계를 파악하기 어려워진다.관계 파악이 어렵다?
라는 의미는?
- 확장하기가 어렵다.
- 세부적인 모델만 이해한 상태로는 코드 수정이 꺼려진다. (전체 모델이 망가질 수 있으므로)
- 변경을 최대한 회피하는 쪽으로 요구사항을 협의하게 된다.
- 장기적으로는 코드를 수정하기 더 어렵게 만든다.
위는 애그리거트 단위로 모델을 묶어서 표현한 것
애그리거트를 사용함으로써 모델 간의 관계를 개별 모델 수준과 상위 수준에서 모두 이해가 가능하다.
- 모델 이해도를 증가시킴
- 일관성을 관리하는 기준이 됨 (한 애그리거트에 속한 객체는 유사하거나 동일한 LifeCycle을 갖는다.)
이것이 애그리거트의 필요성이다.
유의할 점
- A가 B를 갖는다.
- 위와 같은 요구사항이 있다면 A와 B를 한 애그리거트로 묶어서 생각하기 쉽지만 하지만 반드시 옳은 것은 아니다.
- Order가 ShippingInfo(주문 정보)와 Orderer(주문자)를 가지므로 어느정도 타당
- 그러나! 상품 - 리뷰 관계에서 Product 엔티티와 Review 엔티티는 함께 생성되지 않고, 함께 변경되지 않는다.
- Product 변경 주체 : 상품 담당자
- Review 변경 주체 : 고객
- Review의 변경이 Product에 영향을 주지 않고 반대로 Product의 변경이 Review에 영향을 주지 않기 때문에 이 둘은 서로 다른 애그리거트에 속한다.
- 위와 같은 요구사항이 있다면 A와 B를 한 애그리거트로 묶어서 생각하기 쉽지만 하지만 반드시 옳은 것은 아니다.
애그리거트 루트
애그리거트에 속한 모든 객체가 일관된 상태를 유지하려면 애그리거트 전체를 관리할 주체가 필요하다.
이 책임을 지는 것이 바로 애그리거트의 루트 엔티티
이다.
루트 엔티티에서 객체의 상태를 변경하지 않는다면, 데이터 일관성이 깨지기 쉽다. 아래 예를 보자.
주문(Order) 애그리거트는 다음을 포함한다.
- 총 금액인 totalAmounts를 갖는 Order 엔티티
- 개별 구매 상품의 개수인 quantity와 금액인 price를 갖고 있는 OrderLine 밸류
구매할 상품의 개수를 변경하면 OrderLine의 quantity를 변경하고 더불어 Order의 totalAmounts도 변경해야한다.
그렇지 않으면 도메인 규칙을 어기고 데이터 일관성이 깨지게 된다.
- 주문 총 금액 = 개별 상품의 주문 개수 * 가격의 합
만약 Order(루트 엔티티)를 통하여 OrderLine의 quantity를 변경하지 않고, Order.getOrderLine() 으로 OrderLine에 직접 접근하여 quantity를 변경하게 되면, Order의 totalAmounts의 변경을 신경안쓰게 될 가능성이 높다.
도메인 규칙과 일관성
위에서 얘기한 OrderLine과 동일한 내용이다. 애그리거트 내의 도메인 모델의 변경은 애그리거트 루트가 제공하는 메서드를 활용해야 한다. 그렇지 않으면 일관성이 깨질 가능성이 높다.
배송이 시작되기 전까지만 배송지 정보를 변경할 수 있다.
라는 규칙이 있을 때 애그리거트 루트 Order는 이 규칙에 따라 배송 시작 여부를 확인하고 규칙을 충족할 때만 배송지 정보를 변경해야 한다.
class Order{
// 애그리거트 루트는 도메인 규칙을 구현한 기능을 제공한다.
fun changeShippingInfo(ShippingInfo newShippingInfo) {
verifyNotYetShipped()
setShippingInfo(newShippingInfo)
}
fun verifyNotYetShipped() {
if (state != OrderState.PAYMENT_WAITING && state != OrderState.PREPARING)
throw new IllegalStateException("already shipped")
}
}
만약 위의 메소드를 사용하지 않고, 외부에서 애그리거트에 속한 객체를 직접 변경한다면 일관성을 깨는 원인이 된다.
val shippingInfo = order.getShippingInfo()
shippingInfo.setAddress(newAddress)
위 코드는 ShippingInfo를 가져와 직접 정보를 변경하는 코드인데, 주문 상태에 상관없이 배송지 주소를 변경하므로 업무 규칙을 무시하고 데이터를 수정하는 것과 같은 결과를 만든다. (직접 DB 테이블의 데이터를 수정하는 것, 논리적인 데이터 일관성이 깨짐)
일관성을 지키기 위해 Application Layer에서 상태 확인 로직을 구현할수도 있겠으나, 이렇게 되면 동일 검사 로직을 여러 응용 서비스에서 중복으로 구현할 가능성이 높아져 유지보수에 도움이 되지 않는다.
val shippingInfo = order.getShippingInfo()
// 도메인 로직이 중복되는 문제
if (order.state != OrderState.PAYMENT_WAITING && order.state != OrderState.PREPARING) {
throw new IllegalArtumentException()
}
shippingInfo.setAddress(newAddress)
불필요한 중복을 피하고 애그리거트 루트를 통해서만 도메인 로직을 구현하게 만드려면 아래 두 가지를 습관적으로 적용해야 한다.
- 단순히 필드를 변경하는 set 메서드를 공개(public) 범위로 만들지 않는다.
- 밸류 타입은 불변으로 구현한다.
set 형식의 이름을 갖는 공개 메서드를 사용하지 않으면 자연스럽게 cancel이나 changePassword 처럼 의미가 더 잘 드러나는 이름을 사용하는 빈도가 높아진다. -> data class의 프로퍼티를 private으로 모두 적용하면 될런지? 직렬화/역직렬화시 문제가 없을지?
애그리거트 루트의 기능 구현
애그리거트 루트는 애그리거트 내부의 다른 객체를 조합해서 기능을 완성한다.
case1. 애그리거트 루트가 구성요소의 상태를 참조
- Order는 총 주문 금액을 구하기 위해 OrderLine 목록을 사용한다. (
it.price * it.quantity
)
class Order {
private totalAmounts: Money
private orderLines: List<OrderLine>
private fun calculateTotalAmounts() {
val sum = orderLines.stream()
.mapToInt(it.price * it.quantity)
.sum()
this.totalAmounts = Money(sum)
}
}
case2. 구성 요소에게 기능 실행을 위임
- 구현 기술의 제약이나 내부 모델링 규칙 때문에 OrderLines 목록을 별도 클래스로 분리했다고 가정
class OrderLines {
private lines: List<OrderLine>
fun getTotalAmounts(): Money { ... 구현; }
fun changeOrderLines(newLines: List<OrderLine>) = this.lines = newLines
}
- Order의 changeOrderLines() 메서드는 orderLines 필드에 상태 변경을 위임하는 방식으로 구현한다.
class Order {
private totalAmounts: Money
private orderLines: OrderLines
fun changeOrderLines(newLines: List<OrderLine>) {
orderLines.changeOrderLines(newLines)
this.totalAmounts = orderLines.getTotalAmounts()
}
}
주의해야할 점
Order(애그리거트 루트)에서 getOrderLines()와 같이 OrderLines를 구하는 메소드를 제공하면 안된다.
why?
- 외부에서 내부 애그리거트 상태를 변경할 수 있게 됨
- order의 totalAmounts값이 orderLines의 총 금액과 같지 않는 현상이 발생할 수 있음
val orderLines = order.getOrderLines()
// 외부에서 내부 애그리거트
orderLines.changeOrderLines(newOrderLines)
Repository와 Aggregate
애그리거트는 개념상 완전한 한개의 도메인 모델을 표현하므로 객체의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재한다.
Order, OrderLine가 별도의 테이블로 구성되었다고 해서 Order, OrderLine을 위한 리포지터리를 각각 만들지 않는다.
Order가 애그리거트 루트고 OrderLine은 애그리거트에 속하는 구성요소이므로 Order를 위한 리포지터리만 존재한다.
- JPA를 사용하면 관계형 모델에 객체 도메인 모델을 맞춰야할 때도 있음(레거시 DB를 사용하거나 팀 내 DB 설계 표준을 따랴아 한다면)
Order 애그리거트와 관련된 테이블이 세 개라면 Order 애그리거트를 저장할 때 애그리거트에 속하는 모든 구성요소에 매핑된 테이블에 데이터를 저장해야한다.
// 애그리거트 전체 영속화 필요
orderRepository.save(order)
- Repository는 Entity 기준. Entity는 Table 기준인데... 그렇다면 Dao에서 여러개의 Repository를 주입받아서 사용하는 식으로 해야할지?
- Entity + Value + Value ... -> Table
- Entity 저장시, Value 들도 저장
- ㄴㄴ 테이블 두개를 하나의 Entity로 만드는건.. 테이블 자체가 설계가 잘못된듯 싶음
출처) 도메인 주도 개발 시작하기 - 최범균
- 애그리거트 간 집합 연관은 어떻게 하는 것이 좋을까?
- 애그리거트 루트는 엔티티이므로 @Entity로 매핑 설정한다.
- 한 테이블에 엔티티와 밸류 데이터가 같이 있다면?
- 밸류는 @Embeddable로 매핑한다.
- 밸류 타입 프로퍼티는 @Embedded로 매핑 설정한다.
'프로그래밍 노트 > 도메인 주도 설계(DDD)' 카테고리의 다른 글
[도메인 주도 설계] 도메인 모델과 바운디드 컨텍스트 (0) | 2023.08.21 |
---|---|
[도메인 주도 설계] 도메인 서비스(Domain Service) (0) | 2023.08.16 |
[도메인 주도 설계] 표현 영역과 응용 영역(Presentation Layer, Application Layer) (2) | 2023.08.14 |
[도메인 주도 설계] 아키텍처에 관하여 (0) | 2023.07.23 |
[도메인 주도 설계] 도메인이란? (도메인과 친해져보자) (0) | 2023.07.23 |