[Mlog] N+1 문제 해결

노의빈·2023년 11월 16일
1

Mlog

목록 보기
13/13
post-thumbnail

저번엔 지연로딩 시 세션이 사라지는 LazyInitialization 문제가 발생했었다.
해결 과정은 여기를 참고하길 바란다.
위의 과정에서 N+1 문제를 경험하였다. 이에 대한 해결 방법을 기록한다.

🤔 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개의 쿼리가 발생되는 시점만 차이가 존재하고 동일하게 쿼리가 발생하므로 문제를 해결할 수가 없다.
이제 정말 해결 방법을 알아보자.

1. Fetch Join 사용하기

JPQL을 직접 작성하여 조인하는 방식이다. Join 타입을 지정하지 않으면 기본적으로 inner join 이 된다.

select e 
from Entity e join fetch e.RelationEntity r

Join 과 Fetch Join 의 차이

기본 Join과 Fetch Join 의 차이는 아래와 같다.

타입영속화 대상사용 예시
기본 Join주체 Entity만연관 Entity를 Where 조건으로만 사용하는 경우
Fetch Join주체 Entity와 연관 Entity 모두연관된 Entity의 정보를 불러와야할 경우

Fetch Join 을 사용하면 아래와 같이 쿼리가 1개만 발생된다!

PostSeries가 NULL인 경우도 존재하도록 허용하였기에 left join 을 사용하였다.

작성한 JPQL

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을 하는 방식도 존재한다.
방식을 간단히 설명하자면 아래와 같다.

Entity Graph

Repository의 조회 함수 위에 아래와 같이 작성한다.

@EntityGraph(attributePaths = {"연관된 Entity 필드명"})

Named Entity Graph

Entity Graph에 이름을 부여하여 사용하는 방식이다.

주체가 되는 Entity Class 위에 아래와 같이 작성한 후

@NamedEntityGraph(name = "EntityGraph 이름", attributeNodes = @NamedAttributeNode("연관관계 Entity 필드명"))

Repository에 조회 함수 위에 아래와 같이 작성하면 끝이다.

@EntityGraph(attributePaths = {"Entity Graph 이름"})

2. Batch Size 설정하기

사실 이 방법은 완벽한 해결 방법이 아니다.
다만, 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

profile
백엔드 공부 중입니다.

0개의 댓글