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는 트랜잭션 범위 내에서 지연 로딩을 허용하기 때문에
실제로 상태를 변경하는 시점에 필요한 구성요소만 로딩해도 문제가 되지 않는다.
'도메인 주도 개발 시작하기' 카테고리의 다른 글
[도메인 주도 개발 시작하기] 인프라스트럭쳐 개요 (0) | 2025.01.14 |
---|---|
[도메인 주도 개발 시작하기] 애그리거트의 영속성 전파 (0) | 2025.01.14 |
[도메인 주도 개발 시작하기] 바운디드 컨텍스트 (0) | 2025.01.14 |
[도메인 주도 개발 시작하기] 애그리거트 (0) | 2025.01.07 |
[도메인 주도 개발 시작하기] 엔티티와 밸류 (0) | 2025.01.07 |