저번엔 지연로딩 시 세션이 사라지는 LazyInitialization 문제가 발생했었다.
해결 과정은 여기를 참고하길 바란다.
위의 과정에서 N+1 문제를 경험하였다. 이에 대한 해결 방법을 기록한다.
N+1 문제는 쿼리를 JPA에서 Entity
조회 시 연관된 Entity
를 조회하기 위해 추가적으로 쿼리를 발생시키는 것을 말한다(사실 1+N이 맞지 않을까 싶기도 하다..)
N+1 문제는 불필요한 쿼리를 여러번 발생시켜 시스템의 성능을 저하시키는 문제가 발생한다.
아래의 쿼리는 실제로 Mlog의 포스트 목록을 조회하였을 때 발생되는 쿼리다.
Hibernate:
select
p1_0.id,
p1_0.content,
p1_0.post_series,
p1_0.preview_content,
p1_0.thumbnail,
p1_0.title,
p1_0.visible,
p1_0.writing_time
from
post p1_0
where
p1_0.visible
order by
p1_0.id desc
Hibernate:
select
p1_0.id,
p1_0.series
from
post_series p1_0
where
p1_0.id=?
Hibernate:
select
p1_0.id,
p1_0.series
from
post_series p1_0
where
p1_0.id=?
Hibernate:
select
p1_0.id,
p1_0.series
from
post_series p1_0
where
p1_0.id=?
Hibernate:
select
p1_0.id,
p1_0.series
from
post_series p1_0
where
p1_0.id=?
쿼리가 총 5개가 발생되는 것을 알 수 있다.
이 문제를 해결하기 위한 해결 방법을 알아보자.
해결 방법을 알아보기 이전에 즉시 로딩
과 지연 로딩
설정으로 해결이 가능한지 확인해보자.
즉시 로딩과 지연 로딩에 대한 차이는 연관된 Entity를 언제 조회하느냐
다.
즉, N개의 쿼리가 발생되는 시점만 차이가 존재하고 동일하게 쿼리가 발생하므로 문제를 해결할 수가 없다.
이제 정말 해결 방법을 알아보자.
JPQL을 직접 작성하여 조인하는 방식이다. Join 타입을 지정하지 않으면 기본적으로 inner join 이 된다.
select e
from Entity e join fetch e.RelationEntity r
기본 Join과 Fetch Join 의 차이는 아래와 같다.
타입 | 영속화 대상 | 사용 예시 |
---|---|---|
기본 Join | 주체 Entity만 | 연관 Entity를 Where 조건으로만 사용하는 경우 |
Fetch Join | 주체 Entity와 연관 Entity 모두 | 연관된 Entity의 정보를 불러와야할 경우 |
Fetch Join 을 사용하면 아래와 같이 쿼리가 1개만 발생된다!
PostSeries가 NULL인 경우도 존재하도록 허용하였기에 left join 을 사용하였다.
select p
from Post p left join fetch p.postSeries s
where p.visible = true
order by p.id desc
Hibernate:
select
p1_0.id,
p1_0.content,
p2_0.id,
p2_0.series,
p1_0.preview_content,
p1_0.thumbnail,
p1_0.title,
p1_0.visible,
p1_0.writing_time
from
post p1_0
left join
post_series p2_0
on p2_0.id=p1_0.post_series
where
p1_0.visible=1
order by
p1_0.id desc
참고로 @EntityGraph
어노테이션을 이용해 Fetch Join을 하는 방식도 존재한다.
방식을 간단히 설명하자면 아래와 같다.
Repository의 조회 함수 위에 아래와 같이 작성한다.
@EntityGraph(attributePaths = {"연관된 Entity 필드명"})
Entity Graph에 이름을 부여하여 사용하는 방식이다.
주체가 되는 Entity Class 위에 아래와 같이 작성한 후
@NamedEntityGraph(name = "EntityGraph 이름", attributeNodes = @NamedAttributeNode("연관관계 Entity 필드명"))
Repository에 조회 함수 위에 아래와 같이 작성하면 끝이다.
@EntityGraph(attributePaths = {"Entity Graph 이름"})
사실 이 방법은 완벽한 해결 방법이 아니다.
다만, N+1 문제가 발생되기 전에 예방차원으로 설정하게 되면 좋은 설정인 것 같다.
이 방식은 where in 조회를 통해 추가적으로 발생되는 쿼리의 양을 굉장히 많이 줄여준다. 대부분의 DB는 IN절의 최대 개수 값이 1000개이므로, Batch Size를 1000 이하의 값으로 설정하면 된다.
application.yml 에 아래와 같이 설정해주게 되면 된다.
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 1000 # 1000이하의 값
실제로 발생되는 쿼리를 확인해보면 아래와 같다.
Hibernate:
select
p1_0.id,
p1_0.content,
p1_0.post_series,
p1_0.preview_content,
p1_0.thumbnail,
p1_0.title,
p1_0.visible,
p1_0.writing_time
from
post p1_0
where
p1_0.visible
order by
p1_0.id desc
Hibernate:
select
p1_0.id,
p1_0.series
from
post_series p1_0
where
p1_0.id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
https://programmer93.tistory.com/83
https://cobbybb.tistory.com/18