JPA

JPA MultipleBagFetchException에러 해결하기

salmon16 2024. 1. 20. 22:44

개요

동아리 프로젝트에서 Stylist의 정보를 모든 조회하기 위해 Fetch조인을 사용하는 과정에서  JPA의 N + 1문제를 해결하기 위해  Left Join Fetch을 사용하여 해결하려고 했지만 MultipleBagFetchException이 발생했다. 이유를 알아보자

 

상황

일단 Stylist의 관계를 알아보자

Stylist는 0~3개의 Career를 가질 수 있다.

Stylist는 0~3개의 Style을 등록할 수 있다.

Stylist는 0~5개의 StylistService를 등록할 수 있다.

StylistService는 0~2개의 ServiceCategory를 등록할 수 있다.

즉 Stylist에 OneToMany로 연결되어있다.

public class Stylist implements UserDetails {
    
    @OneToMany(mappedBy = "stylist" , cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Career> career;

    @OneToMany(mappedBy = "stylist", orphanRemoval = true) 
    private List<Style> styles = new ArrayList<>();
    
    @OneToMany(mappedBy = "stylist", orphanRemoval = true)
    private List<StylistService> stylistServices = new ArrayList<>();

이때 그냥 Stylist를 조회해서 stylist.getCareer, getStyles, getStylistService를 사용하여 두 명의 Stylist를 조회 시

총 18개의 sql이 생성된다. (stylist의 career, style, stylistService, serviceCategory가 max로 등록되어있을 때) 

만약 Stylist를 여러 명 조회해서 각 정보들을 모두 조회한다면 수많은 sql 쿼리가 N+1문제로 발생할 것이다.

이를 해결하기 위해 fetch join을 사용하려고 아래와 같이 Query를 작성했다.

@Query("SELECT DISTINCT s FROM Stylist s LEFT JOIN FETCH s.styles LEFT JOIN FETCH s.career LEFT JOIN FETCH s.stylistServices WHERE s.id = :stylistId")

 

MultipleBagFetchException이 발생했다.

이 문제는 2개 이상의 OneToMany에 Fetch Join을 하면 발생하는 Exception이다 우리는 3개의 OneToMany에 Fetch Join을 사용했으므로 MultipleBagFetchException가 발생했다.

 

이를 해결하기 위해 하나에만 Fetch Join을 사용하고 나머지에는 Lazy Loading으로 해결하면 Exception은 발생하지 않지만 성능에는 효율적이지 못한다.

  • 실험 결과 위와 StylistService와 Fetch join을 사용하고 나머지는 Lazy Loading을 사용했을 때 이전과 같은 같은 18개의 sql이 생성되었다.
    • 이전 코드는 Stylist의 roles이 fetch join 되어 실행되었지만 이번 코드에는 StylistService가 Fetch Join 되어 roles을 조회하는 sql이 하나 더 발생하기 때문

해결 방법

이를 해결하기 위해 Hibernate default_batch_fetch_size을 사용하자

 

N+1문제는 결국 Stylist를 조회했을 때 연관 관계가 있는 자식 엔티티들의 조회 sql이 많이 나가는 문제이다. 

이 문제는 Stylist의 Id를 이용해서 각 자식 엔티티들을 조회하기 때문에 발생하는 문제이다. (각 Stylist Id를 이용해서 where stylist_id =?로 조회)

 

그럼 여러 명의 Stylist Id를 묶어서 sql의 In절을 사용하면 자식 엔티티를 조회하는 sql문이 줄어들게 될 것이다.

이를 사용하는 옵션이 Hibernate default_batch_fetch_size이다.

 

이 옵션은 yml 파일에서 설정할 수 있다.

jpa:  
    hibernate:      
      default_batch_fetch_size: 100

여기서 size란 in절로 묶을 id 개수를 의미한다.

 

이렇게 조회했을 때 sql이 7개로 줄어든 결과를 볼 수 있다.

생성되는 sql이 아래와 같이 In절이 사용된 것을 확인할 수 있다. 

 

결과

즉 이번 프로젝트에서 2명의 Stylist의 정보를 조회하는 것에도 18번의 sql이 생성되는 것을 Hibernate default_batch_fetch_size를 사용해서 7개의 sql이 생성되는 것으로 성능을 개선할 수 있었다. 

이는 더 많은 Stylist를 조회하면 더 큰 성능이 개선될 것으로 보인다.

 

정리

정리하자면 default_batch_fetch_size를 설명하면 N+1문제를 In 쿼리를 사용해서 최소한의 성능을 보장한다. 

만약 default_batch_fetch_size가 100인데 100명 이상의 Stylist를 조회했을 땐 결국 추가적인 sql이 생성되므로 

Fetch Join을 이용해서 최대한 성능을 보장하고 Fetch Join을 사용하지 못하는 것에 대해서만  default_batch_fetch_size 옵션으로 성능 보장을 해야 한다. 

 

 

'JPA' 카테고리의 다른 글

JPQL(Java Persistence Query Language)  (0) 2024.01.03
값 타입, 값 타입 컬렉션  (0) 2023.09.01
임베디드 타입  (0) 2023.08.31
기본값 타입  (0) 2023.08.31
영속성 전이 : CASCADE, 고아객체  (0) 2023.08.31