본문 바로가기

도메인 주도 개발 시작하기

[도메인 주도 개발 시작하기] 애그리거트 로딩 전략

1) 애그리거트 로딩 전략이란?

- JPA 매핑을 설정할 때, 항상 기억해야 할 점은 애그리거트에 속한 객체가 모두 모여야 완전한 하나가 된다는 것이다. 

  즉, 다음과 같이 애그리거트 루트를 로딩하면 루트에 속한 모든 객체가 완전한 상태여야 함을 의미한다. 

// product는 완전한 하나여야 한다.
Product product = productRepository.findById(id);

 

- 조회 시점에서 애그리거트를 완전한 상태가 되도록 하려면

  애그리거트 루트에서 연관 매핑의 조회 방식을 즉시 로딩(FetchType.EAGER)으로 설정하면 된다.

 

- 다음과 같이 컬렉션이나 @Entity에 대한 매핑의 fetch 속성을 즉시 로딩(FetchType.EAGER)으로 설정하면

  EntityManager#find() 메서드로 애그리거트 루트를 구할 때 연관된 구성 요소를 DB에서 함께 읽어온다. 

 

// @Entity 컬렉션에 대한 즉시 로딩 설정
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval= true, fetch = FetchType.EAGER)
@JoinColumn(name = "product_id")
@OrderColumn(name = "list_idx")
private List<Image> images = new ArrayList<>();

// @Embeddable 컬렉션에 대한 즉시 로딩 설정
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "order_line", joinColumns = @JoinColumn(name = "order_number")
@OrderColumn(name = "line_idx")
private List<OrderLine> orderLines;

 

- 즉시 로딩 방식으로 설정하면 애그리거트 루트를 로딩하는 시점에

  애그리거트에 속한 모든 객체를 함께 로딩할 수 있는 장점이 있지만

  이것이 항상 좋은 것은 아니다.

 

- 특히 컬렉션에 대해 로딩 전략을 FetchType.EAGER로 설정하면

  오히려 즉시 로딩 방식이 문제가 될 수 있다.

  예를 들어, Product 애그리거트 루트가 @Entity로 구현한 Image와 @Embeddable로 구현한

  Option 목록을 갖고 있다고 해보자. 

@Entity
@Table(name = "product")
public class Product { 
   ...
   @OneToMany(
   	    cascade = {CascadeType.PERSIST, CascadeType.REMOVE},
        orphanRemoval = true,
        fetch = FetchType.EAGER)
   @JoinColumn(name = "product_id")
   @OrderColumn(name = "list_idx")
   private List<Image> images = new ArrayList<>();
   
   @ElementCollection(fetch = FetchTeyp.EAGER)
   @CollectionTable(name = "product_option", joinColumns = @JoinColumn(name = "product_id"))
   @OrderColumn(name = "list_idx")
   private List<Option> options = new ArrayList<>(); 
   
   ...
 }

 

- 이 매핑을 사용할 때 EntityManager#find() 메서드로 Product를 조회하면 

  하이버네이트는 다음과 같이 Product를 위한 테이블, Image를 위한 테이블, Option을 위한 테이블을 

  조인한 쿼리를 실행한다. 

select
	p.product_id, ..., img.product_id, img.image_id, img.list_idx, img_image_id, ...,
    opt.product_id, opt.option_title, opt.option_value, opt.list_idx
from
	product p
    left outer join image img on p.product_id=img.product_id
    left outer join product_option opt on p.product_id = opt.product_id
where p.product_id=?

 

- 이 쿼리는 카타시안 조인을 사용하고 이는 쿼리 결과에 중복을 발생시킨다.

  조회하는 Product의 image가 2개고 option이 2개면 

  위 쿼리 결과로 구해지는 행 개수는 4개가 된다.

  product 테이블의 정보는 4번 중복되고 image와 product_option 테이블의 정보는 2번 중복된다.

  

- 물론 하이버네이트가 중복된 데이터를 알맞게 제거해서 실제 메모리에는 

  1개의 Product 객체, 2개의 Image 객체, 2개의 Option 객체로 변환해주지만

  애그리거트가 커지면 문제가 될 수 있다. 

 

- 만약 한 개 제품에 대한 이미지가 20개고, Option이 15개면 

  EntityManager#find() 메서드가 실행하는 쿼리는 300행을 리턴한다.

  실제 필요한 행 개수가 36(1+20+15)개인 것에 비하면 300개는 과도하게 많다.

  

- 보통 조회 성능 문제 때문에 즉시 로딩 방식을 사용하지만

  이렇게 조회되는 데이터 개수가 많아지면

  즉시 로딩 방식을 사용할 때, 성능(실행 빈도, 트래픽, 지연 로딩 시 실행 속도 등)을 

  검토해봐야 한다. 

 

- 애그리거트는 개념적으로 하나여야 한다.

  하지만 루트 엔티티를 로딩하는 시점에 애그리거트에 속한 객체를 모두 로딩해야 하는 것은 아니다.

  애그리거트가 완전해야 하는 이유는 두 가지 정도로 생각해볼 수 있다.

  첫 번째 이유는 상태를 변경하는 기능을 실행할 때 애그리거트 상태가 완전해야 하기 때문이고,

  두 번째 이유는 표현 영역에서 애그리거트의 상태 정보를 보여줄 때 필요하기 때문이다. 

 

- 이 중 두 번째는 별도의 조회 전용 기능과 모델을 구현하는 방식을 사용하는 것이 더 유리하기 때문에

  애그리거트의 완전한 로딩과 관련된 문제는 상태 변경과 더 관련이 있다.

  상태 변경 기능을 실행하기 위해 조회 시점에 즉시 로딩을 이용해서

  애그리거트를 완전한 상태로 로딩할 필요는 없다. 

 

- JPA는 트랜잭션 범위 내에서 지연 로딩을 허용하기 때문에 

  실제로 상태를 변경하는 시점에 필요한 구성요소만 로딩해도 문제가 되지 않는다.