주하니 서하아빠

5장 애그리게잇과 전술적 설계 본문

스터디/도메인 주도 설계 핵심

5장 애그리게잇과 전술적 설계

JUHANPAPA 2023. 8. 20. 17:56

애자일 프로젝트 관리 컨텍스트라는 이름의 핵심 도메인이고, 다른 하나는 컨텍스트 매핑 통합 기반의 협업 도구를 제공하는 지원 서브도메인이다.

(예제) 에그리게잇

  • 데이터 변경의 단위로 다루는 연관 객체의 묶음
  • 애그리게잇은 변경의 단위이다.
  • 애그리거트는 관련 도메인을 하나의 군집으로 묶은 것

애그리거트 장점

  • 모델을 이해하는데 도움을 준다.
  • 일관성을 관리하는 기준이 된다.
  • 복잡한 도메인을 단순한 구조로 만들어준다.
  • 복잡도가 낮아져서 도메인 기능을 확장하고 변경하는데 필요한 노력이 줄어든다.

애그리거트 특징

  • 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다.
  • 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다.
  • 독립된 객체군이며 각 애그리거트는 자기자신을 관리할 뿐 다른 애그리거트는 관리하지 않는다.
  • 경계설정시 기본은 도메인 규칙과 요구사항이다.

애그리게잇

  • 엔티티과 값 객체로 이루어진 집합
  • 각 애그리게잇은 1개 이상의 엔터티로 구성

엔티티, 그러니까 테이블 여러개를 묶어 관리하는 "그룹" 개념인 동시에, 하나의 기능 단위를 추상화해서 표현하는 단위라고 보면 된다.

aggregate에는 중심이 되는 엔티티, aggregate root가 무조건 하나가 존재해야 한다.

그리고 aggregate 간의 상호작용은 aggregate root만을 통해서 이루어져야 하고, aggregate root를 제외한 요소들은 OOP의 원칙에 따라 은닉화가 되어야 한다.

[출처] [DDD] 도메인 주도 설계(Domain Driven Design)

애그리거트의 불변성 유지와 캡슐화

애그리거트는 불변성을 유지하면서 캡슐화를 지켜야한다.

불변성 유지

애그리거트의 상태는 루트 엔티티를 통해 변경되어야 하며, 한번 생성된 애그리거트는 그 상태가 변하지 않는다. 애그리거트의 내부 객체들도 마찬가지로 불변성을 유지해야한다. 이를 통해 애그리거트는 내부의 객체들이 외부에서 영향을 받지 않도록 보호하면서 일관성을 유지할 수 있다.

캡슐화

애그리거트 내부 객체들이 외부에서 직접 접근할 수 없도록 보호하는 것이다. 애그리거트의 내부 객체들은 애그리거트 루트 엔티티를 통해서만 접근할 수 있어야 하고, 이를 통해 애그리거트는 내부의 객체들을 외부에서 보호하면서 일관성을 유지할 수 있다.

불변성 유지와 캡슐화를 지키는 이유는 애그리거트 내부 객체들을 외부로부터 보호하면서 일관성을 유지할 수 있으며, 객체 간 결합도를 낮추어 유지 보수성과 확장성을 높일 수 있다.

애그리거트 루트 엔티티를 구현해보았다 In Kotlin

@Entity
@Table(name = "order")
class Order(
    @Id
    val id: Long,
    val customerName: String,
    @OneToMany(mappedBy = "order", cascade = [CascadeType.ALL], orphanRemoval = true)
    val orderItems: List<OrderItem>
) {
    fun addItem(item: OrderItem) {
        orderItems.add(item)
    }
}

@Entity
@Table(name = "order_item")
class OrderItem(
    @Id
    val id: Long,
    val itemName: String,
    val price: Int,
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    val order: Order
)

Order의 필드를 val로 설정해줌으로써 불변성의 원칙과 addItem을 통해 아이템을 추가하는 로직에서 orderItem은 Order에서만 호출되며 orderItem필드를 직접 조작할 수 없게 설계되어있는 것을 볼 수 있다.

[DDD] 도메인 주도 설계 애그리거트(Aggregate) 알아보기 (velog.io)

DDD, Aggregate Root와 JPA Repository


DDD에서 엔티티(Entity)마다 리파지토리(Repository)를 만드는 경우가 많은데 이럴 때 여러 엔티티를 묶어서 하나의 애그리거트로 만들어서 사용할 수 있다. 이러한 연관 객체의 묶음을 Aggregate라 하고, 특히 여러 엔티티를 묶어서 DB로부터 조회해오는 경우가 많을 땐 Aggregate Root에 해당되는 Entity에 대해서만 Repository를 만들 수 있다.

예를 들어 아래와 같이 각 엔티티별로 연관관계를 가지도록 설계했다고 하자.

Order(주문) 엔티티는 Delivery Address (배송지) 필드를 가지고, Payment(지불정보) 엔티티, Item(상품정보) 엔티티와 연관관계를 가진다. Payment(지불정보)엔티티는 지불 방식에 대한 정보를 가진 Method of Payment 엔티티와 연관관계를 가진다고 하자.

!https://blog.kakaocdn.net/dna/uELaI/btqOzA0YoHy/AAAAAAAAAAAAAAAAAAAAAO57Y0wi416VBZXWEHPq8HfcLQbT_TOz8rwmECx3suh_/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1753973999&allow_ip=&allow_referer=&signature=R6j8K1ZzPwZFX48dL7lmSy7fi30%3D

DDD에서는 위 그림에서 보이는 것과 같은 연관 객체의 묶음을 Aggregate라고 한다. 그리고 Aggregate Root인 Order에 대해서 레포지토리를 만들었다.

즉, Aggregate는 우리가 데이터 변경의 단위로 다루는 연관 객체의 묶음이고, 여기엔 루트(Root)와 경계(Boundary)가 있다. 루트는 하나만 존재하고, Aggregate 내의 특정 엔티티를 가리킨다경계 안의 객체는 서로 참조할 수 있지만, 경계 바깥의 객체는 해당 Aggregate의 구성요소 중 루트에만 참조할 수 있다. (다 위에서 설명했음~)

DDD, Aggregate Root란? + JPA CasecadeType 영속성전이 (tistory.com)

엔터티(Value Object)

: 엔터티는 독립적인 것. 엔터티는 변할 수 있는 것이며, 여러번 아니 항상 그 상태는 계속해서 변할 수 있다. 꼭 변하는 것은 아니고, 변하지 않을 수도 있다.

  • 데이터기반 설계에서 말하는 엔티티와 별 차이가 없다.

그냥 테이블이라고 생각해도 된다.

값 객체는 무엇인가?

  • 엔터티와 달리 고유한 식별성이 없음
  • 엔터티를 서술하고, 수량화 하거나 측정하는데 사용된다.
  • 한번 만들어지면 바뀌지 않는 객체

예를 들어 사람(Person)에 국가라는 개념을 추가하려면, 국가를 뜻하는 값 객체를 만들어 사람(Person) 엔티티와 연결하는 것이다.

이건 별도 테이블로 분리할 수도 있고, 종속된 테이블의 컬럼에 쑤셔넣는 형태로 구현할 수도 있고, Enum 등을 이용한 코드처리로 처리될 수도 있다.

구현형태가 명확하지는 않다.

[출처] [DDD] 도메인 주도 설계(Domain Driven Design)

애그리게잇 형태1

  • 애그리게잇 루트1
  • 엔터티
  • 값 객체

애그리게잇 형태2

  • 애그리게잇 루트2
  • 엔터티 → 값 객체
  • 엔터티
  • 애그리게잇루트 엔터티애그리게잇 안의 다른 모든 요소를 소유한다.
  • 각 애그리게잇은 일관성 있는 트랜잭션 경계를 형성한다. → 즉, 한 트랜잭션에서는 한 개의 애그리거트만 수행해야한다.
  • 2개의 애그리게잇이 정의돼 있는데, 둘 중 하나만 단일 트랜잭션으로 커밋돼야 한다. 즉, 하나의 트랜잭션에는 오직 1개의 애그리게잇만을 수정하고 커밋한다는 것이 애그리게잇 설계 규칙.
  • 또 다른 애그리게잇(애그리게잇 형태2)은 별도 분리된 트랜잭션으로 수정, 커밋된다.
  • 즉, 애그리게잇 형태1과 형태2는 완전히 분리된 트랜잭션 이다.

🔸애그리게잇 경험 법칙🔸

애그리게잇 설계의 네 가지 기본 규칙을 살펴보자

  1. 애그리게잇 경계 내에서 비즈니스 불변사항들을 보호하라.
  2. 작은 애그리게잇을 설계하라.
  3. 오직 ID를 통해 다른 애그리게잇을 참고하라.
  4. 결과적 일관성을 사용해 다른 애그리게잇을 갱신하라.

규칙1: 애그리게잇 경계 내의 비즈니스 불변사항을 보호하라.

트랜잭션이 커밋될 때 비즈니스의 일관성이 지켜지는 것에 기반을 두고 애그리게잇 구성 요소를 결정해야 한다는 의미.

  • 예를 들면 제품 애그리게잇 에서 Product는 트랜잭션의 끝에 ProductBacklogItem 인스턴스로 구성되는 모든 것이 반드시 Product의 루트와 일관되게 처리되도록 설계한다.
  • 다른 예로 BacklogItem 애그리게잇 이 존재하는데, “모든 Task(작업) 인스턴스의 hoursRemaining(남은 시간)이 0일 때, BacklogItem의 status(상태)는 반드시 DONE(완료)으로 설정해야 한다”라는 비즈니스 규칙이 있다. 이는 트랜잭션 후 반드시 부합돼야 하는 매우 명확한 비즈니스 불변사항이다.

규칙 2: 작은 애그리게잇을 설계하라

  • 각 애그리게잇의 메모리 사용량과 트랜잭션 범위가 비교적 작아야 함을 강조한다.
  • Product 애그리게잇을 4개의 작은 애그리게잇으로 분해함. 작은 Product애그리게잇, 작은 BacklogItem 애그리게잇, 작은 Release 애그리게잇 그리고 작은 Sprint 애그리게잇을 얻을 수 있다. → 이들은 빠르게 로드되고, 더 작은 메모리를 차지하며, 가비지 컬렉션도 더 빠르다. → 이전의 큰 클러스터의 Product 애그리게잇보다 훨씬 더 자주, 성공적인 트랜잭션을 수행할 것
  • 애그리게잇을 설계할 때 깊이 새겨둬야 할 또 다른 사항은 SRP(Single Responsibility Principle) 라는 단일 책임의 원칙이다.

규칙 3: 오직 식별자(ID)로만 다른 애그리게잇을 참고하라

  • Product를 4개의 작은 애그리게잇으로 분해 ( Product, BacklogItem, Release, Sprint )
  • 이때 BacklogItem, Release, Sprint 모두가 Product ID를 유지함으로써 Product를 참고함. → 이것은 애그리게잇을 작게 유지하고, 동일한 트랜잭션 내 여러 애그리게잇을 수정하려는 접근을 방지
  • ID 참조를 활용하게 되면, 애그리거트 단위별로 경계가 명확해질 수 있으며 또한 하나의 애그리거트가 다른 애그리거트를 수정할 수 있는 문제를 원천적으로 막을 수 있게 된다.

다른애그리거트 직접참조 문제점

  • 다른 애그리거트 상태를 쉽게 변경할 수 있음
  • 애그리거트간 의존 결합도가 높아짐
  • 성능과 관련된 고민이 필요함(JPA 지연로딩, 즉시로딩 어떤거 쓸지..)
  • 확장의 어려움 (트래픽증가시 도메인별로 시스템을 분리하는데 직접 참조시 확장이 어려움)

규칙 4: 결과적 일관성을 사용해 다른 애그리게잇을 갱신하라

  • BacklogItem 애그리게잇의 트랜잭션의 일부로, BacklogItemCommitted라는 도메인 이벤트를 발행시킨다.
  • BacklogItem 트랜잭션을 완료한 후의 상태는 BacklogItemCommitted 도메인 이벤트를 통해 유지
  • BacklogItemCommitted가 구독자에게 전달되면, 트랜잭션이 시작되고 Sprint의 상태는 할당된 BacklogItem의 BacklogItemId를 보유하도록 수정됨.
  • 도메인 이벤트는 애그리게잇에 의해 발행되고, 이에 관심이 있는 바운디드 컨텍스트는 이를 전달 받는다.

🔸애그리게잇 모델링🔸

  • 빈약한 도메인 모델(Anemic Domain Model)을 조심해야 한다. 이것은 모든 애그리게잇이 비즈니스 행위가 아닌 읽고(getters)쓰는 (setters)공개 접근자만을 갖는 것이다. → 이는 모델링을 하면서 비즈니스 보다 기술적인 부분에 초점을 맞췄을 때 발생하는 경향이 있다.
public class Product : Entity
{
	private string description;
	private string name;
	private ProductId productId; // 동일한 테넌트 내 다른 모든 것들로부터 Product를 구별
	private TenantId tenantId; // 특정한 구독자 조직 내에서 루트 엔터티를 식별
}
  • 만일, 속성 쓰기 메서드를 공개시켜 버린다면, Product의 값 설정을 위한 로직이 모델 밖에 구현될 것이기 때문에 빈약한 도메인으로 쉽게 빠질 수 있다.

추상화를 조심스럽게 선택해라

  • 추상화 수준이 너무 높아지면 각 개별적인 형태의 세부 사항을 모델링하기 시작할 때 어려운 상황에 빠질 수 있음
  • 이것은 각각의 클래스마다 특수한 경우를 정의할 것이고, 명백한 문제들에 대한 일반적인 접근을 통해 복잡한 클래스 계층 구조를 만들 것이다.
  • 우선적으로 중요하지 않은 문제를 해결 하려다가 필요한 것보다 훨씬 많은 코드를 생산할 것이다.
  • 잘못된 추상화 수준은 심지어 사용자 인터페이스까지 영향을 미처 사용자에게 혼란을 주는 경우도 종종 발생

올바른 크기의 애그리게잇

  1. 먼저 애그리게잇 설계의 두 번째 규칙인 “작은 애그리게잇을 설계하라”에 집중하자. 애그리게잇 루트로 제공될 오직 1개의 엔터티만을 갖는 애그리게잇을 생성한다.
  2. 이제 애그리게잇 설계의 첫 번째 규칙인 “애그리게잇 경계 내의 비즈니스 불변사항을 보호하라”로 관심을 돌리자. 애그리게잇 A1을 살펴본다고 할 때, 이미 정의한 다른 애그리게잇들 중에 A1 애그리게잇이 변경될 때 함께 갱신돼야 하는 것이 있는지 도메인 전문가에게 확인한다.
  3. 반응에 맞춘 갱신이 일어나는 시간은 얼마나 걸릴지 도메인 전문가에게 확인하자.
  4. 각각의 애그리게잇들이 즉시 처리돼야 할 경우, 동일한 애그리게잇 경계 안에 그 2개의 엔터티를 구성하는 것을 긍정적으로 검토해야 한다. 예를 들어 애그리게잇A1과 애그리게잇A2를 새로운 애그리게잇 A[1, 2]로 구성한다는 것이다. 이렇게 변경한다면, 이전에 정의했던 애그리게잇 A1과 A2는 더 이상 존재하지 않고, 오직 애그리게잇 A[1, 2]만 존재한다.
  5. 각각의 애그리게잇들이 주어진 시간에 따라 각각 반응하는 경우, 애그리게잇 설계의 네 번째 규칙인 “결과적 일관성을 사용해 다른 애그리게잇을 갱신하라”를 사용해서 갱신한다.

A1의 일관성 규칙 목록에는 A2가 적혀있고, C14는 소요 시간(30초)을 갖는다.

결과적으로, A1과 A2는 하나의 애그리게잇 A[1, 2]안으로 통합 모델링된다. 또한 런타임 중에 애그리게잇 A[1, 2]는 애그리게잇 C14를 갱신하도록 하는 도메인 이벤트를 발행시킨다.

  • 모든 애그리게잇이 함께 즉각적인 갱신에 들어가야 한다고 비즈니스 측에서 주장하지 않는지 주의해야 함. 이해관계자들은 트랜잭션 위주의 관점을 가짐. 하지만 실제로 비즈니스가 모든 상황에 즉각적인 일관성을 요구할 가능성은 매우 낮음.

테스트 가능한 단위

단위 테스트를 위해 애그리게잇을 철저하게 캡슐화되도록 설계하자.

(예제) 에그리게잇

  • 데이터 변경의 단위로 다루는 연관 객체의 묶음
  • 애그리게잇은 변경의 단위이다.
  • 애그리거트는 관련 도메인을 하나의 군집으로 묶은 것

애그리거트 장점

  • 모델을 이해하는데 도움을 준다.
  • 일관성을 관리하는 기준이 된다.
  • 복잡한 도메인을 단순한 구조로 만들어준다.
  • 복잡도가 낮아져서 도메인 기능을 확장하고 변경하는데 필요한 노력이 줄어든다.

애그리거트 특징

  • 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다.
  • 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다.
  • 독립된 객체군이며 각 애그리거트는 자기자신을 관리할 뿐 다른 애그리거트는 관리하지 않는다.
  • 경계설정시 기본은 도메인 규칙과 요구사항이다.

애그리게잇

  • 엔티티과 값 객체로 이루어진 집합
  • 각 애그리게잇은 1개 이상의 엔터티로 구성

엔티티, 그러니까 테이블 여러개를 묶어 관리하는 "그룹" 개념인 동시에, 하나의 기능 단위를 추상화해서 표현하는 단위라고 보면 된다.

aggregate에는 중심이 되는 엔티티, aggregate root가 무조건 하나가 존재해야 한다.

그리고 aggregate 간의 상호작용은 aggregate root만을 통해서 이루어져야 하고, aggregate root를 제외한 요소들은 OOP의 원칙에 따라 은닉화가 되어야 한다.

[출처] [DDD] 도메인 주도 설계(Domain Driven Design)

애그리거트의 불변성 유지와 캡슐화

애그리거트는 불변성을 유지하면서 캡슐화를 지켜야한다.

불변성 유지

애그리거트의 상태는 루트 엔티티를 통해 변경되어야 하며, 한번 생성된 애그리거트는 그 상태가 변하지 않는다. 애그리거트의 내부 객체들도 마찬가지로 불변성을 유지해야한다. 이를 통해 애그리거트는 내부의 객체들이 외부에서 영향을 받지 않도록 보호하면서 일관성을 유지할 수 있다.

캡슐화

애그리거트 내부 객체들이 외부에서 직접 접근할 수 없도록 보호하는 것이다. 애그리거트의 내부 객체들은 애그리거트 루트 엔티티를 통해서만 접근할 수 있어야 하고, 이를 통해 애그리거트는 내부의 객체들을 외부에서 보호하면서 일관성을 유지할 수 있다.

불변성 유지와 캡슐화를 지키는 이유는 애그리거트 내부 객체들을 외부로부터 보호하면서 일관성을 유지할 수 있으며, 객체 간 결합도를 낮추어 유지 보수성과 확장성을 높일 수 있다.

애그리거트 루트 엔티티를 구현해보았다 In Kotlin

@Entity
@Table(name = "order")
class Order(
    @Id
    val id: Long,
    val customerName: String,
    @OneToMany(mappedBy = "order", cascade = [CascadeType.ALL], orphanRemoval = true)
    val orderItems: List<OrderItem>
) {
    fun addItem(item: OrderItem) {
        orderItems.add(item)
    }
}

@Entity
@Table(name = "order_item")
class OrderItem(
    @Id
    val id: Long,
    val itemName: String,
    val price: Int,
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    val order: Order
)

Order의 필드를 val로 설정해줌으로써 불변성의 원칙과 addItem을 통해 아이템을 추가하는 로직에서 orderItem은 Order에서만 호출되며 orderItem필드를 직접 조작할 수 없게 설계되어있는 것을 볼 수 있다.

[DDD] 도메인 주도 설계 애그리거트(Aggregate) 알아보기 (velog.io)

DDD, Aggregate Root와 JPA Repository


DDD에서 엔티티(Entity)마다 리파지토리(Repository)를 만드는 경우가 많은데 이럴 때 여러 엔티티를 묶어서 하나의 애그리거트로 만들어서 사용할 수 있다. 이러한 연관 객체의 묶음을 Aggregate라 하고, 특히 여러 엔티티를 묶어서 DB로부터 조회해오는 경우가 많을 땐 Aggregate Root에 해당되는 Entity에 대해서만 Repository를 만들 수 있다.

예를 들어 아래와 같이 각 엔티티별로 연관관계를 가지도록 설계했다고 하자.

Order(주문) 엔티티는 Delivery Address (배송지) 필드를 가지고, Payment(지불정보) 엔티티, Item(상품정보) 엔티티와 연관관계를 가진다. Payment(지불정보)엔티티는 지불 방식에 대한 정보를 가진 Method of Payment 엔티티와 연관관계를 가진다고 하자.

!https://blog.kakaocdn.net/dna/uELaI/btqOzA0YoHy/AAAAAAAAAAAAAAAAAAAAAO57Y0wi416VBZXWEHPq8HfcLQbT_TOz8rwmECx3suh_/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1753973999&allow_ip=&allow_referer=&signature=R6j8K1ZzPwZFX48dL7lmSy7fi30%3D

DDD에서는 위 그림에서 보이는 것과 같은 연관 객체의 묶음을 Aggregate라고 한다. 그리고 Aggregate Root인 Order에 대해서 레포지토리를 만들었다.

즉, Aggregate는 우리가 데이터 변경의 단위로 다루는 연관 객체의 묶음이고, 여기엔 루트(Root)와 경계(Boundary)가 있다. 루트는 하나만 존재하고, Aggregate 내의 특정 엔티티를 가리킨다경계 안의 객체는 서로 참조할 수 있지만, 경계 바깥의 객체는 해당 Aggregate의 구성요소 중 루트에만 참조할 수 있다. (다 위에서 설명했음~)

DDD, Aggregate Root란? + JPA CasecadeType 영속성전이 (tistory.com)

엔터티(Value Object)

: 엔터티는 독립적인 것. 엔터티는 변할 수 있는 것이며, 여러번 아니 항상 그 상태는 계속해서 변할 수 있다. 꼭 변하는 것은 아니고, 변하지 않을 수도 있다.

  • 데이터기반 설계에서 말하는 엔티티와 별 차이가 없다.

그냥 테이블이라고 생각해도 된다.

값 객체는 무엇인가?

  • 엔터티와 달리 고유한 식별성이 없음
  • 엔터티를 서술하고, 수량화 하거나 측정하는데 사용된다.
  • 한번 만들어지면 바뀌지 않는 객체

예를 들어 사람(Person)에 국가라는 개념을 추가하려면, 국가를 뜻하는 값 객체를 만들어 사람(Person) 엔티티와 연결하는 것이다.

이건 별도 테이블로 분리할 수도 있고, 종속된 테이블의 컬럼에 쑤셔넣는 형태로 구현할 수도 있고, Enum 등을 이용한 코드처리로 처리될 수도 있다.

구현형태가 명확하지는 않다.

[출처] [DDD] 도메인 주도 설계(Domain Driven Design)

애그리게잇 형태1

  • 애그리게잇 루트1
  • 엔터티
  • 값 객체

애그리게잇 형태2

  • 애그리게잇 루트2
  • 엔터티 → 값 객체
  • 엔터티
  • 애그리게잇루트 엔터티애그리게잇 안의 다른 모든 요소를 소유한다.
  • 각 애그리게잇은 일관성 있는 트랜잭션 경계를 형성한다. → 즉, 한 트랜잭션에서는 한 개의 애그리거트만 수행해야한다.
  • 2개의 애그리게잇이 정의돼 있는데, 둘 중 하나만 단일 트랜잭션으로 커밋돼야 한다. 즉, 하나의 트랜잭션에는 오직 1개의 애그리게잇만을 수정하고 커밋한다는 것이 애그리게잇 설계 규칙.
  • 또 다른 애그리게잇(애그리게잇 형태2)은 별도 분리된 트랜잭션으로 수정, 커밋된다.
  • 즉, 애그리게잇 형태1과 형태2는 완전히 분리된 트랜잭션 이다.

🔸애그리게잇 경험 법칙🔸

애그리게잇 설계의 네 가지 기본 규칙을 살펴보자

  1. 애그리게잇 경계 내에서 비즈니스 불변사항들을 보호하라.
  2. 작은 애그리게잇을 설계하라.
  3. 오직 ID를 통해 다른 애그리게잇을 참고하라.
  4. 결과적 일관성을 사용해 다른 애그리게잇을 갱신하라.

규칙1: 애그리게잇 경계 내의 비즈니스 불변사항을 보호하라.

트랜잭션이 커밋될 때 비즈니스의 일관성이 지켜지는 것에 기반을 두고 애그리게잇 구성 요소를 결정해야 한다는 의미.

  • 예를 들면 제품 애그리게잇 에서 Product는 트랜잭션의 끝에 ProductBacklogItem 인스턴스로 구성되는 모든 것이 반드시 Product의 루트와 일관되게 처리되도록 설계한다.
  • 다른 예로 BacklogItem 애그리게잇 이 존재하는데, “모든 Task(작업) 인스턴스의 hoursRemaining(남은 시간)이 0일 때, BacklogItem의 status(상태)는 반드시 DONE(완료)으로 설정해야 한다”라는 비즈니스 규칙이 있다. 이는 트랜잭션 후 반드시 부합돼야 하는 매우 명확한 비즈니스 불변사항이다.

규칙 2: 작은 애그리게잇을 설계하라

  • 각 애그리게잇의 메모리 사용량과 트랜잭션 범위가 비교적 작아야 함을 강조한다.
  • Product 애그리게잇을 4개의 작은 애그리게잇으로 분해함. 작은 Product애그리게잇, 작은 BacklogItem 애그리게잇, 작은 Release 애그리게잇 그리고 작은 Sprint 애그리게잇을 얻을 수 있다. → 이들은 빠르게 로드되고, 더 작은 메모리를 차지하며, 가비지 컬렉션도 더 빠르다. → 이전의 큰 클러스터의 Product 애그리게잇보다 훨씬 더 자주, 성공적인 트랜잭션을 수행할 것
  • 애그리게잇을 설계할 때 깊이 새겨둬야 할 또 다른 사항은 SRP(Single Responsibility Principle) 라는 단일 책임의 원칙이다.

규칙 3: 오직 식별자(ID)로만 다른 애그리게잇을 참고하라

  • Product를 4개의 작은 애그리게잇으로 분해 ( Product, BacklogItem, Release, Sprint )
  • 이때 BacklogItem, Release, Sprint 모두가 Product ID를 유지함으로써 Product를 참고함. → 이것은 애그리게잇을 작게 유지하고, 동일한 트랜잭션 내 여러 애그리게잇을 수정하려는 접근을 방지
  • ID 참조를 활용하게 되면, 애그리거트 단위별로 경계가 명확해질 수 있으며 또한 하나의 애그리거트가 다른 애그리거트를 수정할 수 있는 문제를 원천적으로 막을 수 있게 된다.

다른애그리거트 직접참조 문제점

  • 다른 애그리거트 상태를 쉽게 변경할 수 있음
  • 애그리거트간 의존 결합도가 높아짐
  • 성능과 관련된 고민이 필요함(JPA 지연로딩, 즉시로딩 어떤거 쓸지..)
  • 확장의 어려움 (트래픽증가시 도메인별로 시스템을 분리하는데 직접 참조시 확장이 어려움)

규칙 4: 결과적 일관성을 사용해 다른 애그리게잇을 갱신하라

  • BacklogItem 애그리게잇의 트랜잭션의 일부로, BacklogItemCommitted라는 도메인 이벤트를 발행시킨다.
  • BacklogItem 트랜잭션을 완료한 후의 상태는 BacklogItemCommitted 도메인 이벤트를 통해 유지
  • BacklogItemCommitted가 구독자에게 전달되면, 트랜잭션이 시작되고 Sprint의 상태는 할당된 BacklogItem의 BacklogItemId를 보유하도록 수정됨.
  • 도메인 이벤트는 애그리게잇에 의해 발행되고, 이에 관심이 있는 바운디드 컨텍스트는 이를 전달 받는다.

🔸애그리게잇 모델링🔸

  • 빈약한 도메인 모델(Anemic Domain Model)을 조심해야 한다. 이것은 모든 애그리게잇이 비즈니스 행위가 아닌 읽고(getters)쓰는 (setters)공개 접근자만을 갖는 것이다. → 이는 모델링을 하면서 비즈니스 보다 기술적인 부분에 초점을 맞췄을 때 발생하는 경향이 있다.
public class Product : Entity
{
	private string description;
	private string name;
	private ProductId productId; // 동일한 테넌트 내 다른 모든 것들로부터 Product를 구별
	private TenantId tenantId; // 특정한 구독자 조직 내에서 루트 엔터티를 식별
}
  • 만일, 속성 쓰기 메서드를 공개시켜 버린다면, Product의 값 설정을 위한 로직이 모델 밖에 구현될 것이기 때문에 빈약한 도메인으로 쉽게 빠질 수 있다.

추상화를 조심스럽게 선택해라

  • 추상화 수준이 너무 높아지면 각 개별적인 형태의 세부 사항을 모델링하기 시작할 때 어려운 상황에 빠질 수 있음
  • 이것은 각각의 클래스마다 특수한 경우를 정의할 것이고, 명백한 문제들에 대한 일반적인 접근을 통해 복잡한 클래스 계층 구조를 만들 것이다.
  • 우선적으로 중요하지 않은 문제를 해결 하려다가 필요한 것보다 훨씬 많은 코드를 생산할 것이다.
  • 잘못된 추상화 수준은 심지어 사용자 인터페이스까지 영향을 미처 사용자에게 혼란을 주는 경우도 종종 발생

올바른 크기의 애그리게잇

  1. 먼저 애그리게잇 설계의 두 번째 규칙인 “작은 애그리게잇을 설계하라”에 집중하자. 애그리게잇 루트로 제공될 오직 1개의 엔터티만을 갖는 애그리게잇을 생성한다.
  2. 이제 애그리게잇 설계의 첫 번째 규칙인 “애그리게잇 경계 내의 비즈니스 불변사항을 보호하라”로 관심을 돌리자. 애그리게잇 A1을 살펴본다고 할 때, 이미 정의한 다른 애그리게잇들 중에 A1 애그리게잇이 변경될 때 함께 갱신돼야 하는 것이 있는지 도메인 전문가에게 확인한다.
  3. 반응에 맞춘 갱신이 일어나는 시간은 얼마나 걸릴지 도메인 전문가에게 확인하자.
  4. 각각의 애그리게잇들이 즉시 처리돼야 할 경우, 동일한 애그리게잇 경계 안에 그 2개의 엔터티를 구성하는 것을 긍정적으로 검토해야 한다. 예를 들어 애그리게잇A1과 애그리게잇A2를 새로운 애그리게잇 A[1, 2]로 구성한다는 것이다. 이렇게 변경한다면, 이전에 정의했던 애그리게잇 A1과 A2는 더 이상 존재하지 않고, 오직 애그리게잇 A[1, 2]만 존재한다.
  5. 각각의 애그리게잇들이 주어진 시간에 따라 각각 반응하는 경우, 애그리게잇 설계의 네 번째 규칙인 “결과적 일관성을 사용해 다른 애그리게잇을 갱신하라”를 사용해서 갱신한다.

A1의 일관성 규칙 목록에는 A2가 적혀있고, C14는 소요 시간(30초)을 갖는다.

결과적으로, A1과 A2는 하나의 애그리게잇 A[1, 2]안으로 통합 모델링된다. 또한 런타임 중에 애그리게잇 A[1, 2]는 애그리게잇 C14를 갱신하도록 하는 도메인 이벤트를 발행시킨다.

  • 모든 애그리게잇이 함께 즉각적인 갱신에 들어가야 한다고 비즈니스 측에서 주장하지 않는지 주의해야 함. 이해관계자들은 트랜잭션 위주의 관점을 가짐. 하지만 실제로 비즈니스가 모든 상황에 즉각적인 일관성을 요구할 가능성은 매우 낮음.

테스트 가능한 단위

단위 테스트를 위해 애그리게잇을 철저하게 캡슐화되도록 설계하자.

(예제) 에그리게잇

  • 데이터 변경의 단위로 다루는 연관 객체의 묶음
  • 애그리게잇은 변경의 단위이다.
  • 애그리거트는 관련 도메인을 하나의 군집으로 묶은 것

애그리거트 장점

  • 모델을 이해하는데 도움을 준다.
  • 일관성을 관리하는 기준이 된다.
  • 복잡한 도메인을 단순한 구조로 만들어준다.
  • 복잡도가 낮아져서 도메인 기능을 확장하고 변경하는데 필요한 노력이 줄어든다.

애그리거트 특징

  • 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다.
  • 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다.
  • 독립된 객체군이며 각 애그리거트는 자기자신을 관리할 뿐 다른 애그리거트는 관리하지 않는다.
  • 경계설정시 기본은 도메인 규칙과 요구사항이다.

애그리게잇

  • 엔티티과 값 객체로 이루어진 집합
  • 각 애그리게잇은 1개 이상의 엔터티로 구성

엔티티, 그러니까 테이블 여러개를 묶어 관리하는 "그룹" 개념인 동시에, 하나의 기능 단위를 추상화해서 표현하는 단위라고 보면 된다.

aggregate에는 중심이 되는 엔티티, aggregate root가 무조건 하나가 존재해야 한다.

그리고 aggregate 간의 상호작용은 aggregate root만을 통해서 이루어져야 하고, aggregate root를 제외한 요소들은 OOP의 원칙에 따라 은닉화가 되어야 한다.

[출처] [DDD] 도메인 주도 설계(Domain Driven Design)

애그리거트의 불변성 유지와 캡슐화

애그리거트는 불변성을 유지하면서 캡슐화를 지켜야한다.

불변성 유지

애그리거트의 상태는 루트 엔티티를 통해 변경되어야 하며, 한번 생성된 애그리거트는 그 상태가 변하지 않는다. 애그리거트의 내부 객체들도 마찬가지로 불변성을 유지해야한다. 이를 통해 애그리거트는 내부의 객체들이 외부에서 영향을 받지 않도록 보호하면서 일관성을 유지할 수 있다.

캡슐화

애그리거트 내부 객체들이 외부에서 직접 접근할 수 없도록 보호하는 것이다. 애그리거트의 내부 객체들은 애그리거트 루트 엔티티를 통해서만 접근할 수 있어야 하고, 이를 통해 애그리거트는 내부의 객체들을 외부에서 보호하면서 일관성을 유지할 수 있다.

불변성 유지와 캡슐화를 지키는 이유는 애그리거트 내부 객체들을 외부로부터 보호하면서 일관성을 유지할 수 있으며, 객체 간 결합도를 낮추어 유지 보수성과 확장성을 높일 수 있다.

애그리거트 루트 엔티티를 구현해보았다 In Kotlin

@Entity
@Table(name = "order")
class Order(
    @Id
    val id: Long,
    val customerName: String,
    @OneToMany(mappedBy = "order", cascade = [CascadeType.ALL], orphanRemoval = true)
    val orderItems: List<OrderItem>
) {
    fun addItem(item: OrderItem) {
        orderItems.add(item)
    }
}

@Entity
@Table(name = "order_item")
class OrderItem(
    @Id
    val id: Long,
    val itemName: String,
    val price: Int,
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    val order: Order
)

Order의 필드를 val로 설정해줌으로써 불변성의 원칙과 addItem을 통해 아이템을 추가하는 로직에서 orderItem은 Order에서만 호출되며 orderItem필드를 직접 조작할 수 없게 설계되어있는 것을 볼 수 있다.

[DDD] 도메인 주도 설계 애그리거트(Aggregate) 알아보기 (velog.io)

DDD, Aggregate Root와 JPA Repository


DDD에서 엔티티(Entity)마다 리파지토리(Repository)를 만드는 경우가 많은데 이럴 때 여러 엔티티를 묶어서 하나의 애그리거트로 만들어서 사용할 수 있다. 이러한 연관 객체의 묶음을 Aggregate라 하고, 특히 여러 엔티티를 묶어서 DB로부터 조회해오는 경우가 많을 땐 Aggregate Root에 해당되는 Entity에 대해서만 Repository를 만들 수 있다.

예를 들어 아래와 같이 각 엔티티별로 연관관계를 가지도록 설계했다고 하자.

Order(주문) 엔티티는 Delivery Address (배송지) 필드를 가지고, Payment(지불정보) 엔티티, Item(상품정보) 엔티티와 연관관계를 가진다. Payment(지불정보)엔티티는 지불 방식에 대한 정보를 가진 Method of Payment 엔티티와 연관관계를 가진다고 하자.

!https://blog.kakaocdn.net/dna/uELaI/btqOzA0YoHy/AAAAAAAAAAAAAAAAAAAAAO57Y0wi416VBZXWEHPq8HfcLQbT_TOz8rwmECx3suh_/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1753973999&allow_ip=&allow_referer=&signature=R6j8K1ZzPwZFX48dL7lmSy7fi30%3D

DDD에서는 위 그림에서 보이는 것과 같은 연관 객체의 묶음을 Aggregate라고 한다. 그리고 Aggregate Root인 Order에 대해서 레포지토리를 만들었다.

즉, Aggregate는 우리가 데이터 변경의 단위로 다루는 연관 객체의 묶음이고, 여기엔 루트(Root)와 경계(Boundary)가 있다. 루트는 하나만 존재하고, Aggregate 내의 특정 엔티티를 가리킨다경계 안의 객체는 서로 참조할 수 있지만, 경계 바깥의 객체는 해당 Aggregate의 구성요소 중 루트에만 참조할 수 있다. (다 위에서 설명했음~)

DDD, Aggregate Root란? + JPA CasecadeType 영속성전이 (tistory.com)

엔터티(Value Object)

: 엔터티는 독립적인 것. 엔터티는 변할 수 있는 것이며, 여러번 아니 항상 그 상태는 계속해서 변할 수 있다. 꼭 변하는 것은 아니고, 변하지 않을 수도 있다.

  • 데이터기반 설계에서 말하는 엔티티와 별 차이가 없다.

그냥 테이블이라고 생각해도 된다.

값 객체는 무엇인가?

  • 엔터티와 달리 고유한 식별성이 없음
  • 엔터티를 서술하고, 수량화 하거나 측정하는데 사용된다.
  • 한번 만들어지면 바뀌지 않는 객체

예를 들어 사람(Person)에 국가라는 개념을 추가하려면, 국가를 뜻하는 값 객체를 만들어 사람(Person) 엔티티와 연결하는 것이다.

이건 별도 테이블로 분리할 수도 있고, 종속된 테이블의 컬럼에 쑤셔넣는 형태로 구현할 수도 있고, Enum 등을 이용한 코드처리로 처리될 수도 있다.

구현형태가 명확하지는 않다.

[출처] [DDD] 도메인 주도 설계(Domain Driven Design)

애그리게잇 형태1

  • 애그리게잇 루트1
  • 엔터티
  • 값 객체

애그리게잇 형태2

  • 애그리게잇 루트2
  • 엔터티 → 값 객체
  • 엔터티
  • 애그리게잇루트 엔터티애그리게잇 안의 다른 모든 요소를 소유한다.
  • 각 애그리게잇은 일관성 있는 트랜잭션 경계를 형성한다. → 즉, 한 트랜잭션에서는 한 개의 애그리거트만 수행해야한다.
  • 2개의 애그리게잇이 정의돼 있는데, 둘 중 하나만 단일 트랜잭션으로 커밋돼야 한다. 즉, 하나의 트랜잭션에는 오직 1개의 애그리게잇만을 수정하고 커밋한다는 것이 애그리게잇 설계 규칙.
  • 또 다른 애그리게잇(애그리게잇 형태2)은 별도 분리된 트랜잭션으로 수정, 커밋된다.
  • 즉, 애그리게잇 형태1과 형태2는 완전히 분리된 트랜잭션 이다.

🔸애그리게잇 경험 법칙🔸

애그리게잇 설계의 네 가지 기본 규칙을 살펴보자

  1. 애그리게잇 경계 내에서 비즈니스 불변사항들을 보호하라.
  2. 작은 애그리게잇을 설계하라.
  3. 오직 ID를 통해 다른 애그리게잇을 참고하라.
  4. 결과적 일관성을 사용해 다른 애그리게잇을 갱신하라.

규칙1: 애그리게잇 경계 내의 비즈니스 불변사항을 보호하라.

트랜잭션이 커밋될 때 비즈니스의 일관성이 지켜지는 것에 기반을 두고 애그리게잇 구성 요소를 결정해야 한다는 의미.

  • 예를 들면 제품 애그리게잇 에서 Product는 트랜잭션의 끝에 ProductBacklogItem 인스턴스로 구성되는 모든 것이 반드시 Product의 루트와 일관되게 처리되도록 설계한다.
  • 다른 예로 BacklogItem 애그리게잇 이 존재하는데, “모든 Task(작업) 인스턴스의 hoursRemaining(남은 시간)이 0일 때, BacklogItem의 status(상태)는 반드시 DONE(완료)으로 설정해야 한다”라는 비즈니스 규칙이 있다. 이는 트랜잭션 후 반드시 부합돼야 하는 매우 명확한 비즈니스 불변사항이다.

규칙 2: 작은 애그리게잇을 설계하라

  • 각 애그리게잇의 메모리 사용량과 트랜잭션 범위가 비교적 작아야 함을 강조한다.
  • Product 애그리게잇을 4개의 작은 애그리게잇으로 분해함. 작은 Product애그리게잇, 작은 BacklogItem 애그리게잇, 작은 Release 애그리게잇 그리고 작은 Sprint 애그리게잇을 얻을 수 있다. → 이들은 빠르게 로드되고, 더 작은 메모리를 차지하며, 가비지 컬렉션도 더 빠르다. → 이전의 큰 클러스터의 Product 애그리게잇보다 훨씬 더 자주, 성공적인 트랜잭션을 수행할 것
  • 애그리게잇을 설계할 때 깊이 새겨둬야 할 또 다른 사항은 SRP(Single Responsibility Principle) 라는 단일 책임의 원칙이다.

규칙 3: 오직 식별자(ID)로만 다른 애그리게잇을 참고하라

  • Product를 4개의 작은 애그리게잇으로 분해 ( Product, BacklogItem, Release, Sprint )
  • 이때 BacklogItem, Release, Sprint 모두가 Product ID를 유지함으로써 Product를 참고함. → 이것은 애그리게잇을 작게 유지하고, 동일한 트랜잭션 내 여러 애그리게잇을 수정하려는 접근을 방지
  • ID 참조를 활용하게 되면, 애그리거트 단위별로 경계가 명확해질 수 있으며 또한 하나의 애그리거트가 다른 애그리거트를 수정할 수 있는 문제를 원천적으로 막을 수 있게 된다.

다른애그리거트 직접참조 문제점

  • 다른 애그리거트 상태를 쉽게 변경할 수 있음
  • 애그리거트간 의존 결합도가 높아짐
  • 성능과 관련된 고민이 필요함(JPA 지연로딩, 즉시로딩 어떤거 쓸지..)
  • 확장의 어려움 (트래픽증가시 도메인별로 시스템을 분리하는데 직접 참조시 확장이 어려움)

규칙 4: 결과적 일관성을 사용해 다른 애그리게잇을 갱신하라

  • BacklogItem 애그리게잇의 트랜잭션의 일부로, BacklogItemCommitted라는 도메인 이벤트를 발행시킨다.
  • BacklogItem 트랜잭션을 완료한 후의 상태는 BacklogItemCommitted 도메인 이벤트를 통해 유지
  • BacklogItemCommitted가 구독자에게 전달되면, 트랜잭션이 시작되고 Sprint의 상태는 할당된 BacklogItem의 BacklogItemId를 보유하도록 수정됨.
  • 도메인 이벤트는 애그리게잇에 의해 발행되고, 이에 관심이 있는 바운디드 컨텍스트는 이를 전달 받는다.

🔸애그리게잇 모델링🔸

  • 빈약한 도메인 모델(Anemic Domain Model)을 조심해야 한다. 이것은 모든 애그리게잇이 비즈니스 행위가 아닌 읽고(getters)쓰는 (setters)공개 접근자만을 갖는 것이다. → 이는 모델링을 하면서 비즈니스 보다 기술적인 부분에 초점을 맞췄을 때 발생하는 경향이 있다.
public class Product : Entity
{
	private string description;
	private string name;
	private ProductId productId; // 동일한 테넌트 내 다른 모든 것들로부터 Product를 구별
	private TenantId tenantId; // 특정한 구독자 조직 내에서 루트 엔터티를 식별
}
  • 만일, 속성 쓰기 메서드를 공개시켜 버린다면, Product의 값 설정을 위한 로직이 모델 밖에 구현될 것이기 때문에 빈약한 도메인으로 쉽게 빠질 수 있다.

추상화를 조심스럽게 선택해라

  • 추상화 수준이 너무 높아지면 각 개별적인 형태의 세부 사항을 모델링하기 시작할 때 어려운 상황에 빠질 수 있음
  • 이것은 각각의 클래스마다 특수한 경우를 정의할 것이고, 명백한 문제들에 대한 일반적인 접근을 통해 복잡한 클래스 계층 구조를 만들 것이다.
  • 우선적으로 중요하지 않은 문제를 해결 하려다가 필요한 것보다 훨씬 많은 코드를 생산할 것이다.
  • 잘못된 추상화 수준은 심지어 사용자 인터페이스까지 영향을 미처 사용자에게 혼란을 주는 경우도 종종 발생

올바른 크기의 애그리게잇

  1. 먼저 애그리게잇 설계의 두 번째 규칙인 “작은 애그리게잇을 설계하라”에 집중하자. 애그리게잇 루트로 제공될 오직 1개의 엔터티만을 갖는 애그리게잇을 생성한다.
  2. 이제 애그리게잇 설계의 첫 번째 규칙인 “애그리게잇 경계 내의 비즈니스 불변사항을 보호하라”로 관심을 돌리자. 애그리게잇 A1을 살펴본다고 할 때, 이미 정의한 다른 애그리게잇들 중에 A1 애그리게잇이 변경될 때 함께 갱신돼야 하는 것이 있는지 도메인 전문가에게 확인한다.
  3. 반응에 맞춘 갱신이 일어나는 시간은 얼마나 걸릴지 도메인 전문가에게 확인하자.
  4. 각각의 애그리게잇들이 즉시 처리돼야 할 경우, 동일한 애그리게잇 경계 안에 그 2개의 엔터티를 구성하는 것을 긍정적으로 검토해야 한다. 예를 들어 애그리게잇A1과 애그리게잇A2를 새로운 애그리게잇 A[1, 2]로 구성한다는 것이다. 이렇게 변경한다면, 이전에 정의했던 애그리게잇 A1과 A2는 더 이상 존재하지 않고, 오직 애그리게잇 A[1, 2]만 존재한다.
  5. 각각의 애그리게잇들이 주어진 시간에 따라 각각 반응하는 경우, 애그리게잇 설계의 네 번째 규칙인 “결과적 일관성을 사용해 다른 애그리게잇을 갱신하라”를 사용해서 갱신한다.

A1의 일관성 규칙 목록에는 A2가 적혀있고, C14는 소요 시간(30초)을 갖는다.

결과적으로, A1과 A2는 하나의 애그리게잇 A[1, 2]안으로 통합 모델링된다. 또한 런타임 중에 애그리게잇 A[1, 2]는 애그리게잇 C14를 갱신하도록 하는 도메인 이벤트를 발행시킨다.

  • 모든 애그리게잇이 함께 즉각적인 갱신에 들어가야 한다고 비즈니스 측에서 주장하지 않는지 주의해야 함. 이해관계자들은 트랜잭션 위주의 관점을 가짐. 하지만 실제로 비즈니스가 모든 상황에 즉각적인 일관성을 요구할 가능성은 매우 낮음.

테스트 가능한 단위

단위 테스트를 위해 애그리게잇을 철저하게 캡슐화되도록 설계하자.

 

애그리게잇 부연설명 사이트

DDD - #3 애그리거트(Aggregate)

[ddd] 애그리거트 | 기록은 기억의 연장선 (joont92.github.io)