Dev

N+1 문제 해결해서 성능 개선하기

khjoon 2024. 12. 4. 23:01

문제상황

조회 할 때 쿼리가 조회된 데이터 갯수(n) 만큼 연관관계의 조회 쿼리가 추가로 발생하는 n+1 문제가 발생하였습니다. 쿼리가 추가로 발생해서 성능 저하로 이어지기 때문에 문제를 해결해야 하는 상황입니다.

추가로 발생되는 쿼리 확인

Post가 10개씩 조회되는데 아래의 쿼리가 10개씩 추가로 발생하고 있습니다.

2024-10-23 04:00:12.166 DEBUG 71806 --- [nio-8080-exec-1] org.hibernate.SQL                        : 
    select
        postimgs0_.post_id as post_id4_10_0_,
        postimgs0_.post_img_id as post_img1_10_0_,
        postimgs0_.post_img_id as post_img1_10_1_,
        postimgs0_.name as name2_10_1_,
        postimgs0_.post_id as post_id4_10_1_,
        postimgs0_.url as url3_10_1_ 
    from
        post_img postimgs0_ 
    where
        postimgs0_.post_id=?

원인 파악

JPQL은 객체의 이름으로 쿼리를 하는데 1개의 쿼리로 연관관계는 무시하고 entity들을 조회하고 연관된 entity를 따로 쿼리를 날리게 됩니다.

제 코드에서는 Post를 먼저 찾고 Post와 1:N 연관관계가 있는 PostImg에 N번의 쿼리를 요청하고 있었습니다.

성능비교를 위해 문제해결 전 성능 측정

users 100명이 1분동안 계속 요청 보낸 결과

 

해결 과정

fetch join

SELECT p FROM Post p LEFT JOIN FETCH p.postImgs
fetch join을 사용했더니 N+1 문제는 해결 되었지만 다른 문제가 발생하였습니다.

 

2024-10-23 04:21:41.314  WARN 73399 --- [nio-8080-exec-1] o.h.h.internal.ast.QueryTranslatorImpl   : HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!

 

지금 현재 Post 데이터가 10000개가 있는데 10000개를 전부 조회하고 조회한 데이터에서 Pagination을 하기 때문에 경고가 발생한 것으로 보입니다.

JPA에서 Paging을 하게 되면 OneToMany 관계는 fetch join이 불가능합니다. 왜냐하면 이렇게 일대다 테이블을 조인하면 데이터의 수가 변하기 때문입니다.

 

@Batchsize

연관된 엔티티를 조회할 때 지정된 size 만큼 SQL의 IN절을 사용해서 조회합니다. Post의 갯수만큼 추가 SQL을 날리지 않고, 조회한 postimg의 id들을 모아서 SQL IN 절을 날립니다.

불필요한 쿼리를 방지하는 것을 확인할 수 있었습니다.

select
        postimgs0_.post_id as post_id4_10_1_,
        postimgs0_.post_img_id as post_img1_10_1_,
        postimgs0_.post_img_id as post_img1_10_0_,
        postimgs0_.name as name2_10_0_,
        postimgs0_.post_id as post_id4_10_0_,
        postimgs0_.url as url3_10_0_ 
    from
        post_img postimgs0_ 
    where
        postimgs0_.post_id in (
            ?, ?, ?, ?, ?
        )

개선 후 성능 측정

평균 응답 속도 79ms -> 40ms

 

BatchSize를 사용해서 N+1문제를 해결해보았습니다. 불필요한 쿼리를 줄여서 db에 불필요한 부하를 개선할 수 있었습니다.