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
'트러블슈팅' 카테고리의 다른 글
[Kafka] UnknownHostException 오류 (0) | 2025.03.22 |
---|---|
[SpringBoot] 오류: 기본 클래스 ~를 찾거나 로드할 수 없습니다. (0) | 2025.03.19 |
[redis] ec2 에 설치한 redis와 springboot랑 연결되지 않는 문제 (0) | 2025.03.15 |
[Kafka] ERROR Shutdown broker because all log dirs in C:\tmp\kafka-logs have failed (0) | 2025.02.24 |
[OAuth2] 카카오 로그인페이지가 출력되지 않는 문제 (0) | 2025.02.21 |