📖
Aaron's TECH BOOK
  • Intro
    • About me
  • Lecture
    • Kubernetes
      • Begin Kubernetes
    • Kafka
      • Begin Kafka
    • Kotlin
      • TDD, Clean Code Preview
      • woowa Kotlin
    • Java
      • Multithread Concurrency
      • The Java
    • Toby
      • Toby Spring 6
      • Toby Spring Boot
    • MSA
      • 01.Micro Service
      • 02.DDD 설계
      • 03.DDD 구현
      • 04.EDA 구현
    • Spring Boot
    • Spring Batch
    • Spring Core Advanced
    • Spring DB Part II
    • Spring DB Part I
    • JPA API and Performance Optimization
    • JPA Web Application
    • JPA Programming Basic
    • Spring MVC Part 2
      • 01.Thymeleaf
      • 02.ETC
      • 03.Validation
      • 04.Login
      • 05.Exception
    • Spring MVC Part 1
      • 01.Servlet
      • 02.MVC
    • Http
      • 01.Basic
      • 02.Method
      • 03.Header
    • Spring Core
    • Study
      • Concurrency issues
      • First Come First Served
      • Performance Test
      • TDD
      • IntelliJ
  • Book
    • Kafka Streams in Action
      • 01.카프카 스트림즈
      • 02.카프카 스트림즈 개발
      • 03.카프카 스트림즈 관리
    • Effective Kotlin
      • 01.좋은 코드
      • 02.코드 설계
      • 03.효율성
    • 이벤트 소싱과 MSA
      • 01.도메인 주도 설계
      • 02.객체지향 설계 원칙
      • 03-04.이벤트 소싱
      • 05.마이크로서비스 협업
      • 06.결과적 일관성
      • 07.CQRS
      • 08.UI
      • 09.클라우드 환경
    • 몽고DB 완벽 가이드
      • I. 몽고DB 시작
      • II. 몽고DB 개발
    • Kotlin Cookbook
      • 코틀린 기초
      • 코틀린 기능
      • ETC
    • Kotlin in Action
      • 함수/클래스/객체/인터페이스
      • 람다와 타입
      • 오버로딩과 고차 함수
      • 제네릭스, 애노테이션, 리플렉션
    • Kent Beck Tidy First?
    • 대규모 시스템 설계 기초
      • 01.사용자 수에 따른 규모 확장성
      • 02.개략적인 규모 추정
      • 03.시스템 설계 공략법
      • 04.처리율 제한 장치 설계
      • 05.안정 해시 설계
      • 06.키-값 저장소 설계
      • 07.유일 ID 생성기 설계
      • 08.URL 단축기 설계
      • 09.웹 크롤러 설계
      • 10.알림 시스템 설계
      • 11.뉴스 피드 시스템 설계
      • 12.채팅 시스템 설계
      • 13.검색어 자동완성 시스템
      • 14.유튜브 설계
      • 15.구글 드라이브 설계
      • 16.배움은 계속된다
    • 실용주의 프로그래머📖
    • GoF Design Patterns
    • 도메인 주도 개발 시작하기
      • 01.도메인 모델 시작하기
      • 02.아키텍처 개요
      • 03.애그리거트
      • 04.리포지터리와 모델 구현
      • 05.Spring Data JPA를 이용한 조회 기능
      • 06.응용 서비스와 표현 영역
      • 07.도메인 서비스
      • 08.애그리거트 트랜잭션 관리
      • 09.도메인 모델과 바운디드 컨텍스트
      • 10.이벤트
      • 11.CQRS
    • Effective Java 3/E
      • 객체, 공통 메서드
      • 클래스, 인터페이스, 제네릭
    • 소프트웨어 장인
    • 함께 자라기
    • Modern Java In Action
      • 01.기초
      • 02.함수형 데이터 처리
      • 03.스트림과 람다를 이용한 효과적 프로그래밍
      • 04.매일 자바와 함께
    • Refactoring
      • 01.리펙터링 첫 번째 예시
      • 02.리펙터링 원칙
      • 03.코드에서 나는 악취
      • 06.기본적인 리펙터링
      • 07.캡슐화
      • 08.기능 이동
      • 09.데이터 조직화
      • 10.조건부 로직 간소화
      • 11.API 리팩터링
      • 12.상속 다루기
    • 객체지향의 사실과 오해
      • 01.협력하는 객체들의 공동체
      • 02.이상한 나라의 객체
      • 03.타입과 추상화
      • 04.역할, 책임, 협력
      • 05.책임과 메시지
      • 06.객체 지도
      • 07.함께 모으기
      • 부록.추상화 기법
    • Clean Code
    • 자바 ORM 표준 JPA 프로그래밍
Powered by GitBook
On this page
  • 애그리거트
  • 애그리거트 루트
  • 도메인 규칙과 일관성
  • 트랜잭션 범위
  • 리포지터리와 애그리거트
  • ID를 이용한 애그리거트 참조
  • ID를 이용한 참조와 조회 성능
  • 애그리거트 간 집합 연관
  • 애그리거트를 팩토리로 사용하기
  1. Book
  2. 도메인 주도 개발 시작하기

03.애그리거트

도메인 주도 개발 시작하기 3장을 요약한 내용입니다.

Last updated 1 year ago

애그리거트

개별 객체 수준에서 모델을 바라보면 상위 수준에서 관계를 파악하기 어렵다. 하지만, 상위 수준에서 모델을 정리하면 도메인 모델의 복잡한 관계를 이해하는 데 도움이 된다.

애그리거트는 관련된 객체를 하나의 군으로 묶어 준다. 수많은 객체를 애그리거트로 묶어서 바라보면 상위 수준에서 도메인 모델 간의 관계를 파악할 수 있다.

애그리거트는 복잡한 모델을 관리하는 기준을 제공.

한 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다.

애그리거트는 독립된 객체 군이며 각 애그리거트는 자기 자신을 관리할 뿐 다른 애그리거트를 관리하지 않는다.

상품이 리뷰를 갖는 것으로 생각할 수 있지만, 상품과 리뷰는 함께 생성되거나 변경되지 않고, 변경 주체도 다르기 때문에 서로 다른 애그리거트에 속한다.

애그리거트 루트

애그리거트에 속한 모든 객체가 일관된 상태를 유지하려면 애그리거트 전체를 관리할 주체가 필요하다.

이 책임을 지는 것이 바로 애그리거트의 루트 엔티티이다.

  • 주문 애그리거트에서 루트 역할을 하는 엔티티는 Order.

  • 주문 애그리거트에 속한 모델은 Order에 직/간접적으로 속한다.

도메인 규칙과 일관성

애그리거트 루트의 핵심 역할은 애그리거트의 일관성이 깨지지 않도록 하는 것이다.

  • 애그리거트 외부에서 애그리거트에 속한 객체를 직접 변경하면 안 된다.

불필요한 중복을 피하고 애그리거트 루트를 통해서만 도메인 로직을 구현하게 만들려면 도메인 모델에 대해 습관적으로 적용해야 할 것이 있다.

  • 단순히 필드를 변경하는 set 메서드를 공개 범위로 만들지 않기

    • 대신 의미가 드러나는 메서드를 사용해서 구현하자(ex. cancel, changePassword)

  • 밸류 타입은 불변으로 구현하기

    • 밸류 객체의 값을 변경하려면 새로운 밸류 객체를 할당하자

    • 불변으로 구현할 수 없다면, 밸류의 변경 기능을 package/protected 범위로 한정해서 외부 실행 불가하도록 제한하자

트랜잭션 범위

트랜잭션 범위는 작을수록 좋다.

동일하게 한 트랜잭션에서는 한 개의 애그리거트만 수정해야 한다.

  • 한 트랜잭션에서 두 개 이상의 애그리거트를 수정하면 트랜잭션 충돌 발생 가능성이 더 높이진다.

  • 때문에 한 번에 수정하는 애그리거트 개수가 많아질수록 전체 처리량이 떨어지게 된다.

애그리거트는 최대한 서로 독립적이어야 하는데 한 애그리거트가 다른 애그리거트의 기능에 의존하기 시작하면 애그리거트 간 결합도가 높아진다.

  • 결합도가 높아질수록 향후 수정 비용이 증가하므로 애그리거트에서 다른 애그리거트 상태를 변경하지 말자

만일 부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트 수정이 필요하다면 애그리거트에서 다른 애그리거트를 직접 수정하지 말고, 응용 서비스에서 두 애그리거트를 수정하도록 구현하자.

public class ChangeOrderService {

    @Transactional
    public void changeShippingInfo(...) {
        Order order = orderRepository.findById(id);
        order.shipTo(newShippingInfo);

        ..
        Member member = findMember(order.getOrderer());
        member.changeAddress(newShippingInfo.getAddress());
        ...
    }
}

다음의 경우 한 트랜잭션에서 두 개 이상의 애그리거트를 변경하는 것을 고려

  • 팀 표준: 팀이나 조직의 표준에 따라 사용자 유스케이스와 관련된 응용 서비스의 기능을 한 트랜잭션으로 실행해야 하는 경우

  • 기술 제약: 기줄적으로 이벤트 방식을 도입할 수 없는 경우 한 트랜잭션에서 다수의 애그리거트를 수정해서 일관성을 처리

  • UI 구현의 편리: 운영자의 편리함을 위해 주문 목록 화면에서 여러 주문의 상태를 한 번에 변경하고 싶을 경우

리포지터리와 애그리거트

리포지터리는 애그리거트 단위로 존재

리포지터리에 애그리거트를 저장하면 애그리거트 전체를 영속화해야 한다.

  • 애그리거트 루트와 매핑되는 테이블뿐만 아니라 애그리거트에 속한 모든 구성요소에 매핑된 테이블에 데이터를 저장해야 한다.

  • 동일하게 애그리거트를 구하는 리포지터리 메서드는 완전한 애그리거트를 제공해야 한다.

// 애그리거트 전체를 영속화
orderRepository.save(order);

...

// 리포지터리는 완전한 order 제공
Order order = orderRepository.findById(orderId);
// 그렇지 않다면 기능 실행 도중 NPE와 같은 문제 발생
order.cancel()

데이터의 일관성을 깨지 않으려면 애그리거트의 상태가 변경되면 모든 변경을 원자적으로 저장소에 반영해야 한다.

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

public class Order {
    private Orderer ordere;
    ...
}

public class Orderer {
    private Member member;
    ...
}

public class Member {
  ...
}

order.getOrderer().getMember().getId();

orderer.getMember().changeAddress(newShippingInfo.getAddress());

필드를 이용한 애그리거트 참조는 다음 문제를 야기할 수 있다.

  • 편한 탐색 오용

    • 다른 애그리거트를 수정하고자 하는 유혹

  • 성능에 대한 고민

    • JPA를 이용하면 참조한 객체를 지연(lazy) 로딩과 즉시(eager) 로딩 중 고민

    • 연관 객체 데이터를 함께 보여줘야 한다면 즉시 로딩이 유리

    • 애그리거트 상태 변경 기능을 싱핼할 경우 불필요한 객체 로딩을 막기 위한 지연 로딩이 유리

  • 확장 어려움

    • 도메인마다 서로 다른 DBMS를 사용할 경우

이 문제들은 ID를 이용해서 다른 애그리거트를 참조하면 해결할 수 있다.

  • ID 참조를 사용하면 모든 객체가 참조로 연결되는 것이 아닌 한 애그리거트에 속한 객체들로만 참조로 연결

  • 애그리거트의 경계를 명확히 하고 애그리거트 간 물리적인 연결을 제거하므로 모델의 복잡도를 낮춰준다.

  • 애그리거트 간의 의존을 제거하므로 응집도를 높여주는 효과도 있다.

  • 구현 복잡도가 낮아진다.

  • 다른 애그리거트를 직접 참조하지 않으므로 애그리거트 간 참조를 지연/즉시 로딩 중에 고민을 하지 않아도 된다.

public class Order {
    private Orderer ordere;
    ...
}

public class Orderer {
    private MemberId memberId;
    ...
}

public class Member {
  private MemberId id;
  ...
}

Member member = memberRepository.findById(order.getOrderer().getMemberId());
member.changeAddress(newShippingInfo.getAddress());

ID로 애리거트를 참조하면 리포지터리마다 다른 저장소를 사용하도록 구현할 때 확장이 용이하다.

  • 주문 애그리거트와 같은 중요한 데이터는 RDBMS에 저장

  • 조회 성능이 중요한 상품 애그리거트는 NoSQL에 저장

  • 각 도메인을 별도 프로세스로 서비스하도록 구현 가능

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

다른 애그리거트를 ID로 참조하면 참조하는 여러 애그리거트를 읽을 때 조회 속도 문제가 발생할 수 있다.

  • 조인을 이용해서 한 번에 모든 데이터를 가져올 수 있음에도 주문마다 상품 정보를 읽어오는 쿼리를 실행하게 된다.

ID를 이용한 애그리거트 참조는 지연 로딩과 같은 효과를 만드는 데 지연 로딩과 관련된 대표적인 문제가 N+1 조회 문제이다.

  • 조회 대상이 N개일 때 N개를 읽어오는 한 번(1)의 쿼리와 연관된 데이터(N)를 읽어오는 쿼리를 N번 실행

ID 참조 방식을 사용하면서 N+1 조회와 같은 문제가 발생하지 않도록 하려면 조회 전용 쿼리를 사용하자.

  • 데이터 조회를 위한 별도 DAO를 만돌고, DAO의 조회 메서드에서 조인을 이용해 한 번의 쿼리로 필요한 데이터를 로딩

  • 즉시 로딩이나 지연 로딩과 같은 로딩 전략을 고민할 필요없이 한 번의 쿼리로 로딩 가능

한 대의 DB 장비로 대응할 수 없는 수준의 트래픽이 발생하는 경우 캐시나 조회 전용 저장소는 필수로 선택해야 하는 기법

  • 코드가 복잡해질 수 있지만 시스템의 처리량을 높일 수 있는 장점이 있다.

애그리거트 간 집합 연관

애그리거트 간 1-N 관계는 Set과 같은 컬렉션을 이용해서 표현 가능

public class Category {
    // 다른 애그리거트에 대한 1-N 연관(성능에 심각한 문제)
    private Set<Product> products; 
    ...

    public List<Product> getProducts(int page, int size) {
      List<Product> sortedProducts = sortById(products);
      return sortedProducts.subList((page - 1) * size, page * size);
    }
}

하지만, 카테고리에 속한 모든 상품을 조회하려면, 상품 개수가 수만 개 정도로 많을 경우 실행 속도가 급혁히 느려져 성능에 심각한 문제를 일으킬 수 있다.

  • 개념적으로 애그리거트 간 1-N 연관이 있더라도 이러한 성능 문제로 애그리거트 간의 1-N 연관을 실제 구현에 반영하지 않는다.

카테고리에 속한 상품을 구할 필요가 있다면 상품 입장에서 자신이 속한 카테고리를 N-1로 연관 지어 구하자.

public class Product {
    private CategoryId categoryId;
    ...
}

...

public class ProductListService {
	public Page<Product> getProductOfCategory(Long categoryId, int page, int size) {
		Category category = categoryRepository.findById(categoryId);
		checkCategory(category);
    // categoryId가 지정한 카테고리 식별자인 Product 목록 조회
		List<Product> products = productRepository.findByCategoryId(category.getId(), page, size);
		int totalCount = productRepository.countsByCategoryId(category.getId());
		return new Page(page, size, totalCount, products);
	}
}

M-N 연관은 개념적으로 양쪽 애그리거트에 컬렉션으로 연관을 만든다.

  • 하지만, 상품이 속한 모든 카테고리가 필요한 화면은 상품 상세 화면이다.

  • 개념적으로는 상품과 카테고리의 양방향 M-N 연관이 존재하지만, 실제 구현에서는 상품에서 카테고리로의 단방향 M-N 연관만 적용하면 된다.

public class Product {
    private Set<CategoryId> categoryIds;
    ...
}

JPA를 이용하면 매핑 설정을 사용해서 ID 참조를 이용한 M-N 단방향 연관 구현이 가능하다.

  • JPQL member of 연산자를 이용해서 특정 카테고리에 속한 상품 목록을 구하는 기능 구현이 가능

@Entity
@Table(name = "product")
public class Product {
	@EmbeddedId
	private ProductId id;

	@ElementCollection
	@CollectionTable(name = "product_category",
							joinColumns = @JoinColumn(name = "product_id"))
	private Set<CategoryId> categoryIds; // 밸류 타입에 대한 컬렉션 매핑
  ...
}

...

TypedQuery<Product> query = entityManager.createQuery(
      "select p from Product p "+
      // categoryIds 컬렉션에 catId로 지정한 값이 존재하는지 검사
      "where :catId member of p.categoryIds order by p.id.id desc", 
      Product.class);
query.setParameter("catId", catId);
query.setFirstResult((page - 1) * size);
query.setMaxResults(size);
return query.getResultList();

애그리거트를 팩토리로 사용하기

중요한 도메인 로직 처리가 응용 서비스에 노출되어 있는 경우를 보자.

public class RegisterProductService {
    public ProductId registerNewProduct(NewProductRequest req) {
        Store store = storeRepository.findStoreById(req.getStoreId());
        checkNull(store);
        // Product 생성 가능 판단
        if (!store.isBlocked()) {
            throw new StoreBlockedException();
        }
        // Product 생성
        ProductId id = productRepository.nextId();
        Product product = new Product(id, store.getId(), ...);
        productRepository.save(product);
        return id;
    }
}

해당 기능을 Store 애그리거트에 구현할 수도 있다.

  • createProduct()는 Product 애그리거트를 생성하는 팩토리 역할을 한다.

  • 팩토리 역할을 하면서도 중요한 도메인 로직을 구현하고 있다.

  • 이제 응용 서비스는 팩토리 기능을 이용해서 Product를 생성하면 된다.

public class Store {
    public Product createProduct(ProductId newProductId, ... ) {
        if (isBlocked()) {
            throw new StoreBlockedException();
        }
        return new Product(newProductId, getId(), ...);
    }
}

...

public class RegisterProductService {
    public ProductId registerNewProduct(NewProductRequest req) {
        Store store = storeRepository.findStoreById(req.getStoreId());
        checkNull(store);
        ProductId id = productRepository.nextId();
        Product product = store.createProduct(id, store.getId(), ...);
        productRepository.save(product);
        return id;
    }
}

애그리거트를 팩토리로 사용하면서

  • 응용 서비스에서 더 이상 Store 상태를 확인하지 않아도 된다.

  • Product 생성 가능 여부 확인을 위한 도메인 로직이 변경되어도 응용 서비스는 영향을 받지 않는다.

  • 도메인의 응집도가 높아졌다.

애그리거트가 가지고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면 애그리거트에 팩토리 메서드 구현을 고려해 보자.

Store 애그리거트가 Product 애그리거트를 생성할 때 많은 정보를 알아야 한다면, Store 애그리거트에서 Product 애그리거트를 직접 생성하지 않고 다른 팩토리에 위임하는 방법도 있다.

public class Store {
    public Product createProduct(ProductId newProductId, ProductInfo pi) {
        if (isBlocked()) {
            throw new StoreBlockedException();
        }
        return ProductFactory.create(newProductId, getId(), pi);
    }
}