트러블슈팅

[JPA] N+1 문제 발생 및 MultipleBagFetchException 발생

공부 기록장 2025. 3. 28. 22:48

N+1 이란?

N+1 문제란, ORM(Object-Relational Mapping) 프레임워크에서 하나의 조회 쿼리를 실행했을 때, 추가적으로 N개의 서브 쿼리가 실행되는 비효율적인 데이터 조회 문제를 의미한다.

 

 

 

1. N+1 문제 발생

게시글을 출력하는 과정에서 posts 객체와 연관된 객체들이 N개의 쿼리들로 조회되는 문제가 발생하였다.

Hibernate: 
    select
        p1_0.id,
        p1_0.content,
        p1_0.created_at,
        p1_0.image_path,
        p1_0.opinion,
        p1_0.stock_tickers_id,
        p1_0.updated_at,
        p1_0.users_id 
    from
        posts p1_0 
    where
        p1_0.stock_tickers_id is null 
    order by
        p1_0.created_at desc
Hibernate: 
    select
        u1_0.id,
        u1_0.created_at,
        u1_0.email,
        u1_0.nick_name,
        u1_0.profile_picture,
        u1_0.provider_id,
        u1_0.provider_type,
        u1_0.role,
        u1_0.updated_at,
        u1_0.user_id,
        u1_0.user_pw 
    from
        users u1_0 
    where
        u1_0.id=?
Hibernate: 
    select
        l1_0.posts_id,
        l1_0.id,
        l1_0.comments_id,
        l1_0.users_id 
    from
        post_likes l1_0 
    where
        l1_0.posts_id=?
Hibernate: 
    select
        c1_0.posts_id,
        c1_0.id,
        c1_0.content,
        c1_0.created_at,
        c1_0.updated_at,
        c1_0.users_id 
    from
        comments c1_0 
    where
        c1_0.posts_id=?
Hibernate: 
    select
        l1_0.posts_id,
        l1_0.id,
        l1_0.comments_id,
        l1_0.users_id 
    from
        post_likes l1_0 
    where
        l1_0.posts_id=?
Hibernate: 
    select
        c1_0.posts_id,
        c1_0.id,
        c1_0.content,
        c1_0.created_at,
        c1_0.updated_at,
        c1_0.users_id 
    from
        comments c1_0 
    where
        c1_0.posts_id=?

 

 

 

 

2. N+1 문제 해결 시도 도중 오류 발생

@Query("SELECT p FROM Posts p " +
            "JOIN FETCH p.users u " +
            "LEFT JOIN FETCH p.comments c " +
            "LEFT JOIN FETCH p.likes l " +
            "WHERE p.stockTickers IS NULL " +
            "ORDER BY p.createdAt DESC")
List<Posts> findByStockTickersIsNullOrderByCreatedAtDesc();

기본 게시글을 출력하기 위해 1개의 쿼리만 실행하기 위해 위 코드를 적용했을 때 아래와 같은 에러 메시지가 출력된다.

 

java.lang.IllegalArgumentException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.mysite.stockburning.entity.Posts.comments, com.mysite.stockburning.entity.Posts.likes]
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:146)
	at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:151)
	at org.hibernate.query.Query.getResultList(Query.java:120)

 

 

 

 

3. 오류 원인

Hibernate에서 두 개 이상의 @OneToMany List<> 컬렉션을 동시에 JOIN FETCH 할 경우 발생하는 오류로 아래 Posts 엔티티에서는 comments 와  likes 를 동시에 JOIN FETCH 하고 있어서 발생하는 오류였다.

 

 

 

 

 

4. 해결 방법

1. List 를 Set 으로 변경

  • 엔티티 구성 상 가장 최근에 작성된 댓글(Comments)은 순서가 보장되어야 하므로 List 를 유지하고 좋아요(PostLikes)는 순서가 중요하지 않으므로 Set 으로 변경

2. JOIN FETCH 하나만 사용하고 나머지는 @BatchSize 사용

  • @OneToOne, @ManyToOne와 같은 관계의 자식 엔티티에 대해서는 모두 Fetch Join을 적용하여 한방 쿼리를 수행
  • @OneToMany, @ManyToMany와 같은 관계의 자식 엔티티에 관해서는 가장 데이터가 많은 자식쪽에 Fetch Join을 사용

 

 

 

나의 경우는 [ 1. List 를 Set 으로 변경 ] 을 선택하였다.

 

 

 

5. 결과

 

더 이상 N개의 쿼리가 실행되지 않는 걸 확인할 수 있었다.

Hibernate: 
    select
        p1_0.id,
        c1_0.posts_id,
        c1_0.id,
        c1_0.content,
        c1_0.created_at,
        c1_0.updated_at,
        c1_0.users_id,
        p1_0.content,
        p1_0.created_at,
        p1_0.image_path,
        l1_0.posts_id,
        l1_0.id,
        l1_0.users_id,
        p1_0.opinion,
        p1_0.stock_tickers_id,
        p1_0.updated_at,
        p1_0.users_id,
        u1_0.id,
        u1_0.created_at,
        u1_0.email,
        u1_0.nick_name,
        u1_0.profile_picture,
        u1_0.provider_id,
        u1_0.provider_type,
        u1_0.role,
        u1_0.updated_at,
        u1_0.user_id,
        u1_0.user_pw 
    from
        posts p1_0 
    join
        users u1_0 
            on u1_0.id=p1_0.users_id 
    left join
        post_likes l1_0 
            on p1_0.id=l1_0.posts_id 
    left join
        comments c1_0 
            on p1_0.id=c1_0.posts_id 
    where
        p1_0.stock_tickers_id is null 
    order by
        p1_0.created_at desc