새소식

JPA

MultipleBagFetchException 해결할래 말래

  • -

안할래

 

농담이고요. 해결기 보시죠.

 

레지고

 

문제는 그렇게 발생했습니다.

 

일단 엔티티간 연관관계부터 보시죠.

 

 

지금 개발하고 있는 챗봇 서비스는 회원 한명이 여러 히스토리를, 히스토리 하나가 여러 질문을, 한 질문에는 하나의 답변이, 하나의 답변에는 여러 이미지가 딸려있는 괴랄한 구조를 가집니다.

 

쉽게 생각하자면 지피티의 구조를 생각하시면 될 것 같습니다.

 

그래서 회원이 저 히스토리를 딱 클릭하면 히스토리 정보와 거기에 딸린 질문, 답변, 그리고 이미지를 다 불러와야 할 것 같은데요.

 

연관관계를 다 Lazy Loading으로 설정해놓았기 때문에 그냥 사용했다가는 N+1 폭격을 맞을 것 같네요.

 

그래서 시원하게 fetch로 다 묶어버렸습니다.

 

public interface HistoryRepository extends JpaRepository<History, Long> {

    @Query("select h from History h " +
           "left join fetch h.questions q " +
           "left join fetch q.answer a " +
           "left join fetch a.images " +
           "where h.id = :historyId")
    Optional<History> findHistoryWithDetailsById(@Param("historyId") Long historyId);
    
}

 

캬 역시 난 천재야

 

그러고 테스트를 해봤는데

 

org.hibernate.loader.MultipleBagFetchException:
cannot simultaneously fetch multiple bags:
[knu_chatbot.entity.Answer.images, knu_chatbot.entity.History.questions]

 

이런 건 본 적 없는데요

알아보니 2개 이상의 OneToMany 자식 테이블에 fetch를 걸어버리면 발생하는 예외라네요.

 

저 같은 경우는 history-question 그리고 answer-image 로 OneToMany가 2개 있어 존재해서 발생했습니다.

 

이런 경우는 크게 2가지 방법으로 해결 가능합니다.

 

1. 쿼리 분리

 

간단하게 그냥 2번 조회하도록 쿼리를 분리해버리면 됩니다. 근데 아무래도 코드가 더 복잡해지겠죠.

그래서 전 2번 방법을 선택했습니다.

 

2. default_batch_fetch_size 설정

 

이번에 처음 알게된 설정인데 진짜 신기합니다.

 

기존에 N+1 문제는 다음과 같이 발생합니다.

 

 

history 하나당 question 여러개가 검사되고 다시 또 question과 1대1 매칭되는 answer가 하나당 image 여러개가 검사되죠?

 

이때 그럼 default_batch_fetch_size 설정을 세팅하면 어떻게 될까요?

 

 

default_batch_fetch_size 만큼 지정된 숫자만큼 in절에 부모키(answer_id)가 넘어가서 자식 엔티티들이 조회됩니다.

 

@Query("select h from History h " +
        "left join fetch h.questions q " +
        "left join fetch q.answer a " +
        "where h.id = :historyId")
History findHistoryWithQuestionsAndAnswers(@Param("historyId") Long historyId);

 

이때 기존 쿼리에서 answer-image를 fetch 조회하는 부분은 삭제해야 in절로 조회하기가 적용됩니다.

 

 

저는 1000개로 설정해서 그런지 많이 있네요.

 

더 놀라운 점은 저 default_batch_fetch_size 설정 하나만으로 애플리케이션 전반의 다른 지연 로딩 N+1 문제도 함께 해결해주는 효과를 얻을 수 있습니다.

 

 

저는 application.properties에

spring.jpa.properties.hibernate.default_batch_fetch_size=1000

 

이렇게 설정해뒀는데요, 너무 큰 숫자를 주면 in절 파라미터가 너무 많이 들어가서 문제가 발생할 수 있다고 합니다. 그래서 보통 옵션값을 1000으로 준다고 하네요.

 

그치만 너무 default_batch_fetch_size 옵션에 의존하진 말고 fetch join으로 최소한의 문제를 해결하고 나서 default_batch_fetch_size을 사용하도록 합시다.

 

오늘은 여기까지.

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.