본문 바로가기
개발/Spring

[Spring] JPA Fetch Join 사용시, MultipleBagFetchException 발생

by baau 2023. 8. 3.

SWM 프로젝트를 하면서, Fetch Join을 사용하여 N+1 문제가 발생하지 않고 연관 관계로 연결되어 있는 엔티티를 조회하려고 했었다.

Book 엔티티에는 @ManyToOne 관계로 BookCategory를 가지고 있고, @OneToMany(mappedBy= "book") 관계로는 bookAuthor와 BookTranslator로 가지고 있다.

 

따라서 Fetch Join을 통해서 Book 엔티티를 조회할 때, Book과 연관 관계를 맺고 있는 BookCategory, BookAuthor, BookTranslator 그리고 BookAuthor와 연관관계를 맺고 있는 Author, BookTranslator와 연관관계를 맺고 있는 Translator까지 모두 조회하려고 했었다! N+1 문제가 발생하지 않도록..!

 

@Entity
class Book {
		
    ...

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
    private BookCategory bookCategory;

    @Builder.Default
    @OneToMany(mappedBy = "book", cascade = CascadeType.PERSIST)
    private List<BookAuthor> bookAuthors = new ArrayList<>();

    @Builder.Default
    @OneToMany(mappedBy = "book", cascade = CascadeType.PERSIST)
    private List<BookTranslator> bookTranslators = new ArrayList<>();
}

 

@Query("select b from Book b "
        + "join fetch b.bookAuthors ba "
        + "join fetch ba.author baa "
        + "join fetch b.bookTranslators bt "
        + "join fetch bt.translator btt "
        + "join fetch b.bookCategory bc "
        + "where b.isbn13 = :isbn13")
Optional<Book> findByISBN13WithAllDetailUsingFetchJoin(String isbn13);

 

따라서 위와 같이 JPQL을 작성하였고, 테스트 코드를 돌려본 결과 아래와 같은 예외가 발생하였다.

org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags:

 

예외 발생 원인과 해결 방안에 대해서는 향로님의 블로그를 보고 배울 수 있었다. (https://jojoldu.tistory.com/457)

 

JPA에서 Fetch Join 조건으로 XToOne으로 연관되어 있는 엔티티는 몇 개든 조회가능하지만, XToMany로 연관되어 있는 엔티티는 1개만 가능하다. 따라서 OneToMany로 BookAuthor와 BookTranslator 2개를 Fetch Join을 통해서 가져오려고 하니 MultipleBagFetchException 에러가 발생한 것이다.

 

해결 방안으로는 Hibernate default_batch_fetch_size를 조절하면, Fetch Join 보다는 성능이 조금 떨어지지만, N+1 문제가 발생하지 않으며 최소한의 성능을 보장할 수 있다.

Hibernate.default_batch_fetch_size를 글로벌 설정으로 사용하면 N+1 문제를 최대한 in 쿼리로 기본적인 성능을 보장할 수 있다.

실제 N+1 문제는 부모 엔티티와 연관 관계가 있는 자식 엔티티를 하나하나 조회함으로써 발생하는 문제인데, batch_fetch_size를 통해서 in 절을 사용하여 쿼리를 줄일 수 있다. 만약 batch_fetch_size가 1000으로 설정되어 있고, 자식 엔티티가 1000개 라면, in 절 쿼리를 통해서 하나의 쿼리로 1000개의 자식 엔티티를 가져올 수 있게 되는 것이다. 따라서 최소한으로 성능을 보장할 수 있다.

 

Hibernate.default_batch_fetch_size 설정 방법 (application.yml 에 추가)

spring:
  jpa:
    properties:
      hibernate.default_batch_fetch_size: 1000


그렇담 batch_fetch_size를 설정했다면, fetch join을 사용하지 않아도 될까?? 

정답은 아니다.!! default_batch_fetch_size 옵션을 통해서 최소한의 성능을 보장한 거지 해당 방법은 항상 최선일 수 없다.

따라서 fetch join을 통해서 최대한 성능 튜닝을 진행하고 fetch join으로 해결이 되지 않는 쿼리에 대해서 batch_fetch_size를 통해서 최소한의 성능을 보장하는 것이 좋다!

 

만약 XToMany로 연관되어 있는 엔티티가 여러 개 있고 fetch join을 사용하려고 할 때, 데이터가 가장 많은 엔티티를 fetch join을 통해서 가져오고, 나머지 엔티티에 대해서는 batch_fetch_size를 통해서 in 쿼리로 성능을 보장하면 된다.

 

따라서 다음과 같이 JPQL을 수정하고, batch_fetch_size를 1000으로 설정함으로써, MultipleBagFetchException 예외를 해결할 수 있었다.

 

@Query("select b from Book b "
        + "join fetch b.bookAuthors ba "
        + "join fetch ba.author baa "
        + "join fetch b.bookCategory bc "
        + "where b.isbn13 = :isbn13 or b.isbn10 = :isbn10")
Optional<Book> findByISBNWithAuthorAndCategoryUsingFetchJoin(String isbn10, String isbn13);