문제 배경
내가 작성한 리뷰를 조회하는 API를 스프링 데이터 JPA의 페이징 처리를 통해 구현했다.
현재 API를 호출했을 때 실행되는 SQL 쿼리문을 보면 N+1 문제가 발생한다.
- 실행되는 SQL 쿼리문
2024-02-11 18:31:29.479 INFO 29300 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-02-11 18:31:29.479 INFO 29300 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2024-02-11 18:31:29.483 INFO 29300 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 4 ms
2024-02-11 18:31:29.491 INFO 29300 --- [nio-8080-exec-1] t.OTTify.oauth.jwt.JwtAuthFilter : checkAccessTokenAndAuthentication() 호출
2024-02-11 18:31:29.491 INFO 29300 --- [nio-8080-exec-1] tavebalak.OTTify.oauth.jwt.JwtService : extractAccessToken() 호출
Hibernate:
select
review0_.review_id as review_i1_10_,
review0_.created_at as created_2_10_,
review0_.updated_at as updated_3_10_,
review0_.content as content4_10_,
review0_.genre as genre5_10_,
review0_.like_counts as like_cou6_10_,
review0_.program_id as program_8_10_,
review0_.rating as rating7_10_,
review0_.user_id as user_id9_10_
from
review review0_
left outer join
`user` user1_
on review0_.user_id=user1_.user_id
where
user1_.user_id=?
order by
review0_.created_at asc,
review0_.created_at asc limit ?
2024-02-11 18:31:29.562 INFO 29300 --- [nio-8080-exec-1] p6spy : #1707643889562 | took 7ms | statement | connection 4| url jdbc:mysql://proddb-mysql.c1y4skgea7wb.ap-northeast-2.rds.amazonaws.com:3306/OTTifyDB
select review0_.review_id as review_i1_10_, review0_.created_at as created_2_10_, review0_.updated_at as updated_3_10_, review0_.content as content4_10_, review0_.genre as genre5_10_, review0_.like_counts as like_cou6_10_, review0_.program_id as program_8_10_, review0_.rating as rating7_10_, review0_.user_id as user_id9_10_ from review review0_ left outer join `user` user1_ on review0_.user_id=user1_.user_id where user1_.user_id=? order by review0_.created_at asc, review0_.created_at asc limit ?
select review0_.review_id as review_i1_10_, review0_.created_at as created_2_10_, review0_.updated_at as updated_3_10_, review0_.content as content4_10_, review0_.genre as genre5_10_, review0_.like_counts as like_cou6_10_, review0_.program_id as program_8_10_, review0_.rating as rating7_10_, review0_.user_id as user_id9_10_ from review review0_ left outer join `user` user1_ on review0_.user_id=user1_.user_id where user1_.user_id=1 order by review0_.created_at asc, review0_.created_at asc limit 11;
Hibernate:
select
reviewrevi0_.review_id as review_i2_11_0_,
reviewrevi0_.review_review_tag_id as review_r1_11_0_,
reviewrevi0_.review_review_tag_id as review_r1_11_1_,
reviewrevi0_.review_id as review_i2_11_1_,
reviewrevi0_.review_tag_id as review_t3_11_1_
from
review_review_tag reviewrevi0_
where
reviewrevi0_.review_id=?
2024-02-11 18:31:29.587 INFO 29300 --- [nio-8080-exec-1] p6spy : #1707643889587 | took 6ms | statement | connection 4| url jdbc:mysql://proddb-mysql.c1y4skgea7wb.ap-northeast-2.rds.amazonaws.com:3306/OTTifyDB
select reviewrevi0_.review_id as review_i2_11_0_, reviewrevi0_.review_review_tag_id as review_r1_11_0_, reviewrevi0_.review_review_tag_id as review_r1_11_1_, reviewrevi0_.review_id as review_i2_11_1_, reviewrevi0_.review_tag_id as review_t3_11_1_ from review_review_tag reviewrevi0_ where reviewrevi0_.review_id=?
select reviewrevi0_.review_id as review_i2_11_0_, reviewrevi0_.review_review_tag_id as review_r1_11_0_, reviewrevi0_.review_review_tag_id as review_r1_11_1_, reviewrevi0_.review_id as review_i2_11_1_, reviewrevi0_.review_tag_id as review_t3_11_1_ from review_review_tag reviewrevi0_ where reviewrevi0_.review_id=4;
Hibernate:
select
reviewtag0_.review_tag_id as review_t1_12_0_,
reviewtag0_.name as name2_12_0_
from
review_tag reviewtag0_
where
reviewtag0_.review_tag_id=?
2024-02-11 18:31:29.601 INFO 29300 --- [nio-8080-exec-1] p6spy : #1707643889601 | took 5ms | statement | connection 4| url jdbc:mysql://proddb-mysql.c1y4skgea7wb.ap-northeast-2.rds.amazonaws.com:3306/OTTifyDB
select reviewtag0_.review_tag_id as review_t1_12_0_, reviewtag0_.name as name2_12_0_ from review_tag reviewtag0_ where reviewtag0_.review_tag_id=?
select reviewtag0_.review_tag_id as review_t1_12_0_, reviewtag0_.name as name2_12_0_ from review_tag reviewtag0_ where reviewtag0_.review_tag_id=3;
Hibernate:
select
user0_.user_id as user_id1_14_0_,
user0_.created_at as created_2_14_0_,
user0_.updated_at as updated_3_14_0_,
user0_.average_rating as average_4_14_0_,
user0_.code as code5_14_0_,
user0_.email as email6_14_0_,
user0_.grade as grade7_14_0_,
user0_.nick_name as nick_nam8_14_0_,
user0_.profile_photo as profile_9_14_0_,
user0_.role as role10_14_0_,
user0_.social_type as social_11_14_0_,
user0_.review_counts as review_12_14_0_
from
`user` user0_
where
user0_.user_id=?
2024-02-11 18:31:29.612 INFO 29300 --- [nio-8080-exec-1] p6spy : #1707643889612 | took 10ms | statement | connection 4| url jdbc:mysql://proddb-mysql.c1y4skgea7wb.ap-northeast-2.rds.amazonaws.com:3306/OTTifyDB
select user0_.user_id as user_id1_14_0_, user0_.created_at as created_2_14_0_, user0_.updated_at as updated_3_14_0_, user0_.average_rating as average_4_14_0_, user0_.code as code5_14_0_, user0_.email as email6_14_0_, user0_.grade as grade7_14_0_, user0_.nick_name as nick_nam8_14_0_, user0_.profile_photo as profile_9_14_0_, user0_.role as role10_14_0_, user0_.social_type as social_11_14_0_, user0_.review_counts as review_12_14_0_ from `user` user0_ where user0_.user_id=?
select user0_.user_id as user_id1_14_0_, user0_.created_at as created_2_14_0_, user0_.updated_at as updated_3_14_0_, user0_.average_rating as average_4_14_0_, user0_.code as code5_14_0_, user0_.email as email6_14_0_, user0_.grade as grade7_14_0_, user0_.nick_name as nick_nam8_14_0_, user0_.profile_photo as profile_9_14_0_, user0_.role as role10_14_0_, user0_.social_type as social_11_14_0_, user0_.review_counts as review_12_14_0_ from `user` user0_ where user0_.user_id=1;
Hibernate:
select
program0_.program_id as program_1_7_0_,
program0_.average_rating as average_2_7_0_,
program0_.created_year as created_3_7_0_,
program0_.poster_path as poster_p4_7_0_,
program0_.review_count as review_c5_7_0_,
program0_.title as title6_7_0_,
program0_.tm_db_program_id as tm_db_pr7_7_0_,
program0_.type as type8_7_0_
from
program program0_
where
program0_.program_id=?
2024-02-11 18:31:29.636 INFO 29300 --- [nio-8080-exec-1] p6spy : #1707643889636 | took 22ms | statement | connection 4| url jdbc:mysql://proddb-mysql.c1y4skgea7wb.ap-northeast-2.rds.amazonaws.com:3306/OTTifyDB
select program0_.program_id as program_1_7_0_, program0_.average_rating as average_2_7_0_, program0_.created_year as created_3_7_0_, program0_.poster_path as poster_p4_7_0_, program0_.review_count as review_c5_7_0_, program0_.title as title6_7_0_, program0_.tm_db_program_id as tm_db_pr7_7_0_, program0_.type as type8_7_0_ from program program0_ where program0_.program_id=?
select program0_.program_id as program_1_7_0_, program0_.average_rating as average_2_7_0_, program0_.created_year as created_3_7_0_, program0_.poster_path as poster_p4_7_0_, program0_.review_count as review_c5_7_0_, program0_.title as title6_7_0_, program0_.tm_db_program_id as tm_db_pr7_7_0_, program0_.type as type8_7_0_ from program program0_ where program0_.program_id=1;
Hibernate:
select
reviewrevi0_.review_id as review_i2_11_0_,
reviewrevi0_.review_review_tag_id as review_r1_11_0_,
reviewrevi0_.review_review_tag_id as review_r1_11_1_,
reviewrevi0_.review_id as review_i2_11_1_,
reviewrevi0_.review_tag_id as review_t3_11_1_
from
review_review_tag reviewrevi0_
where
reviewrevi0_.review_id=?
2024-02-11 18:31:29.643 INFO 29300 --- [nio-8080-exec-1] p6spy : #1707643889643 | took 5ms | statement | connection 4| url jdbc:mysql://proddb-mysql.c1y4skgea7wb.ap-northeast-2.rds.amazonaws.com:3306/OTTifyDB
select reviewrevi0_.review_id as review_i2_11_0_, reviewrevi0_.review_review_tag_id as review_r1_11_0_, reviewrevi0_.review_review_tag_id as review_r1_11_1_, reviewrevi0_.review_id as review_i2_11_1_, reviewrevi0_.review_tag_id as review_t3_11_1_ from review_review_tag reviewrevi0_ where reviewrevi0_.review_id=?
select reviewrevi0_.review_id as review_i2_11_0_, reviewrevi0_.review_review_tag_id as review_r1_11_0_, reviewrevi0_.review_review_tag_id as review_r1_11_1_, reviewrevi0_.review_id as review_i2_11_1_, reviewrevi0_.review_tag_id as review_t3_11_1_ from review_review_tag reviewrevi0_ where reviewrevi0_.review_id=5;
Hibernate:
select
reviewtag0_.review_tag_id as review_t1_12_0_,
reviewtag0_.name as name2_12_0_
from
review_tag reviewtag0_
where
reviewtag0_.review_tag_id=?
2024-02-11 18:31:29.650 INFO 29300 --- [nio-8080-exec-1] p6spy : #1707643889650 | took 6ms | statement | connection 4| url jdbc:mysql://proddb-mysql.c1y4skgea7wb.ap-northeast-2.rds.amazonaws.com:3306/OTTifyDB
select reviewtag0_.review_tag_id as review_t1_12_0_, reviewtag0_.name as name2_12_0_ from review_tag reviewtag0_ where reviewtag0_.review_tag_id=?
select reviewtag0_.review_tag_id as review_t1_12_0_, reviewtag0_.name as name2_12_0_ from review_tag reviewtag0_ where reviewtag0_.review_tag_id=4;
Hibernate:
select
reviewtag0_.review_tag_id as review_t1_12_0_,
reviewtag0_.name as name2_12_0_
from
review_tag reviewtag0_
where
reviewtag0_.review_tag_id=?
2024-02-11 18:31:29.666 INFO 29300 --- [nio-8080-exec-1] p6spy : #1707643889666 | took 15ms | statement | connection 4| url jdbc:mysql://proddb-mysql.c1y4skgea7wb.ap-northeast-2.rds.amazonaws.com:3306/OTTifyDB
select reviewtag0_.review_tag_id as review_t1_12_0_, reviewtag0_.name as name2_12_0_ from review_tag reviewtag0_ where reviewtag0_.review_tag_id=?
select reviewtag0_.review_tag_id as review_t1_12_0_, reviewtag0_.name as name2_12_0_ from review_tag reviewtag0_ where reviewtag0_.review_tag_id=5;
Hibernate:
select
program0_.program_id as program_1_7_0_,
program0_.average_rating as average_2_7_0_,
program0_.created_year as created_3_7_0_,
program0_.poster_path as poster_p4_7_0_,
program0_.review_count as review_c5_7_0_,
program0_.title as title6_7_0_,
program0_.tm_db_program_id as tm_db_pr7_7_0_,
program0_.type as type8_7_0_
from
program program0_
where
program0_.program_id=?
2024-02-11 18:31:29.690 INFO 29300 --- [nio-8080-exec-1] p6spy : #1707643889690 | took 22ms | statement | connection 4| url jdbc:mysql://proddb-mysql.c1y4skgea7wb.ap-northeast-2.rds.amazonaws.com:3306/OTTifyDB
select program0_.program_id as program_1_7_0_, program0_.average_rating as average_2_7_0_, program0_.created_year as created_3_7_0_, program0_.poster_path as poster_p4_7_0_, program0_.review_count as review_c5_7_0_, program0_.title as title6_7_0_, program0_.tm_db_program_id as tm_db_pr7_7_0_, program0_.type as type8_7_0_ from program program0_ where program0_.program_id=?
select program0_.program_id as program_1_7_0_, program0_.average_rating as average_2_7_0_, program0_.created_year as created_3_7_0_, program0_.poster_path as poster_p4_7_0_, program0_.review_count as review_c5_7_0_, program0_.title as title6_7_0_, program0_.tm_db_program_id as tm_db_pr7_7_0_, program0_.type as type8_7_0_ from program program0_ where program0_.program_id=3;
2024-02-11 18:31:29.698 INFO 29300 --- [nio-8080-exec-1] p6spy : #1707643889698 | took 7ms | commit | connection 4| url jdbc:mysql://proddb-mysql.c1y4skgea7wb.ap-northeast-2.rds.amazonaws.com:3306/OTTifyDB
;
총 2개의 리뷰를 조회하는데 실행되는 쿼리문의 수는 총 9개다.
엔티티 연관관계
리뷰 엔티티의 연관관계는 다음과 같다.
- 리뷰와 유저
다대일 단방향 관계다. - 리뷰와 프로그램
다대일 단방향 관계다. - 리뷰와 리뷰태그
일대다 양방향 관계다. 리뷰리뷰태그라는 중간 테이블이 존재한다.
성능 최적화
페이징을 성능 최적화하는 방법은 다음과 같다.
- xToOne 관계를 모두 Fetch Join한다.
- 컬렉션은 지연 로딩으로 조회하고, 지연 로딩 성능 최적화를 위해 BatchSize를 적용한다.
- hibernate.default_batch_fetch_size: 글로벌 설정
- @BatchSize: 개별 최적화
1. xToOne 관계 Fetch Join
유저와 프로그램에 대해 Fetch Join을 적용할 수 있다.
하지만 스프링 데이터 JPA에서 @Query 어노테이션을 통해 fetch join할 때 하나의 엔티티만 지정할 수 있는듯하다. 따라서 여기서는 프로그램에 대해서만 페치조인을 수행한다.
QueryDsl이나 JDBC를 이용하면 두 엔티티에 대해 페치조인이 가능할 듯 싶다.
@Query("select r from Review r join fetch r.program where r.user.id =:userId")
Slice<Review> findByUserIdOrderByCreatedAt(Long userId, Pageable pageable);
2. BatchSize 적용
spring:
jpa:
open-in-view: true
hibernate:
ddl-auto: validate
show-sql: true
properties:
hibernate.format_sql: true
dialect: org.hibernate.dialect.MySQL8InnoDBDialect
hibernate:
default_batch_fetch_size: 10
application.yml 파일을 통해 글로벌로 batch size를 설정한다.
최적화 결과
2024-02-11 18:10:04.035 INFO 28784 --- [nio-8080-exec-5] t.OTTify.oauth.jwt.JwtAuthFilter : checkAccessTokenAndAuthentication() 호출
2024-02-11 18:10:04.036 INFO 28784 --- [nio-8080-exec-5] tavebalak.OTTify.oauth.jwt.JwtService : extractAccessToken() 호출
Hibernate:
select
review0_.review_id as review_i1_10_0_,
program1_.program_id as program_1_7_1_,
review0_.created_at as created_2_10_0_,
review0_.updated_at as updated_3_10_0_,
review0_.content as content4_10_0_,
review0_.genre as genre5_10_0_,
review0_.like_counts as like_cou6_10_0_,
review0_.program_id as program_8_10_0_,
review0_.rating as rating7_10_0_,
review0_.user_id as user_id9_10_0_,
program1_.average_rating as average_2_7_1_,
program1_.created_year as created_3_7_1_,
program1_.poster_path as poster_p4_7_1_,
program1_.review_count as review_c5_7_1_,
program1_.title as title6_7_1_,
program1_.tm_db_program_id as tm_db_pr7_7_1_,
program1_.type as type8_7_1_
from
review review0_
inner join
program program1_
on review0_.program_id=program1_.program_id
where
review0_.user_id=?
order by
review0_.created_at asc limit ?
2024-02-11 18:10:04.104 INFO 28784 --- [nio-8080-exec-5] p6spy : #1707642604103 | took 9ms | statement | connection 5| url jdbc:mysql://proddb-mysql.c1y4skgea7wb.ap-northeast-2.rds.amazonaws.com:3306/OTTifyDB
select review0_.review_id as review_i1_10_0_, program1_.program_id as program_1_7_1_, review0_.created_at as created_2_10_0_, review0_.updated_at as updated_3_10_0_, review0_.content as content4_10_0_, review0_.genre as genre5_10_0_, review0_.like_counts as like_cou6_10_0_, review0_.program_id as program_8_10_0_, review0_.rating as rating7_10_0_, review0_.user_id as user_id9_10_0_, program1_.average_rating as average_2_7_1_, program1_.created_year as created_3_7_1_, program1_.poster_path as poster_p4_7_1_, program1_.review_count as review_c5_7_1_, program1_.title as title6_7_1_, program1_.tm_db_program_id as tm_db_pr7_7_1_, program1_.type as type8_7_1_ from review review0_ inner join program program1_ on review0_.program_id=program1_.program_id where review0_.user_id=? order by review0_.created_at asc limit ?
select review0_.review_id as review_i1_10_0_, program1_.program_id as program_1_7_1_, review0_.created_at as created_2_10_0_, review0_.updated_at as updated_3_10_0_, review0_.content as content4_10_0_, review0_.genre as genre5_10_0_, review0_.like_counts as like_cou6_10_0_, review0_.program_id as program_8_10_0_, review0_.rating as rating7_10_0_, review0_.user_id as user_id9_10_0_, program1_.average_rating as average_2_7_1_, program1_.created_year as created_3_7_1_, program1_.poster_path as poster_p4_7_1_, program1_.review_count as review_c5_7_1_, program1_.title as title6_7_1_, program1_.tm_db_program_id as tm_db_pr7_7_1_, program1_.type as type8_7_1_ from review review0_ inner join program program1_ on review0_.program_id=program1_.program_id where review0_.user_id=1 order by review0_.created_at asc limit 11;
Hibernate:
select
reviewrevi0_.review_id as review_i2_11_1_,
reviewrevi0_.review_review_tag_id as review_r1_11_1_,
reviewrevi0_.review_review_tag_id as review_r1_11_0_,
reviewrevi0_.review_id as review_i2_11_0_,
reviewrevi0_.review_tag_id as review_t3_11_0_
from
review_review_tag reviewrevi0_
where
reviewrevi0_.review_id in (
?, ?
)
2024-02-11 18:10:04.115 INFO 28784 --- [nio-8080-exec-5] p6spy : #1707642604115 | took 6ms | statement | connection 5| url jdbc:mysql://proddb-mysql.c1y4skgea7wb.ap-northeast-2.rds.amazonaws.com:3306/OTTifyDB
select reviewrevi0_.review_id as review_i2_11_1_, reviewrevi0_.review_review_tag_id as review_r1_11_1_, reviewrevi0_.review_review_tag_id as review_r1_11_0_, reviewrevi0_.review_id as review_i2_11_0_, reviewrevi0_.review_tag_id as review_t3_11_0_ from review_review_tag reviewrevi0_ where reviewrevi0_.review_id in (?, ?)
select reviewrevi0_.review_id as review_i2_11_1_, reviewrevi0_.review_review_tag_id as review_r1_11_1_, reviewrevi0_.review_review_tag_id as review_r1_11_0_, reviewrevi0_.review_id as review_i2_11_0_, reviewrevi0_.review_tag_id as review_t3_11_0_ from review_review_tag reviewrevi0_ where reviewrevi0_.review_id in (4, 5);
Hibernate:
select
reviewtag0_.review_tag_id as review_t1_12_0_,
reviewtag0_.name as name2_12_0_
from
review_tag reviewtag0_
where
reviewtag0_.review_tag_id in (
?, ?, ?
)
2024-02-11 18:10:04.124 INFO 28784 --- [nio-8080-exec-5] p6spy : #1707642604124 | took 9ms | statement | connection 5| url jdbc:mysql://proddb-mysql.c1y4skgea7wb.ap-northeast-2.rds.amazonaws.com:3306/OTTifyDB
select reviewtag0_.review_tag_id as review_t1_12_0_, reviewtag0_.name as name2_12_0_ from review_tag reviewtag0_ where reviewtag0_.review_tag_id in (?, ?, ?)
select reviewtag0_.review_tag_id as review_t1_12_0_, reviewtag0_.name as name2_12_0_ from review_tag reviewtag0_ where reviewtag0_.review_tag_id in (3, 4, 5);
Hibernate:
select
user0_.user_id as user_id1_14_0_,
user0_.created_at as created_2_14_0_,
user0_.updated_at as updated_3_14_0_,
user0_.average_rating as average_4_14_0_,
user0_.code as code5_14_0_,
user0_.email as email6_14_0_,
user0_.grade as grade7_14_0_,
user0_.nick_name as nick_nam8_14_0_,
user0_.profile_photo as profile_9_14_0_,
user0_.role as role10_14_0_,
user0_.social_type as social_11_14_0_,
user0_.review_counts as review_12_14_0_
from
`user` user0_
where
user0_.user_id=?
2024-02-11 18:10:04.131 INFO 28784 --- [nio-8080-exec-5] p6spy : #1707642604131 | took 5ms | statement | connection 5| url jdbc:mysql://proddb-mysql.c1y4skgea7wb.ap-northeast-2.rds.amazonaws.com:3306/OTTifyDB
select user0_.user_id as user_id1_14_0_, user0_.created_at as created_2_14_0_, user0_.updated_at as updated_3_14_0_, user0_.average_rating as average_4_14_0_, user0_.code as code5_14_0_, user0_.email as email6_14_0_, user0_.grade as grade7_14_0_, user0_.nick_name as nick_nam8_14_0_, user0_.profile_photo as profile_9_14_0_, user0_.role as role10_14_0_, user0_.social_type as social_11_14_0_, user0_.review_counts as review_12_14_0_ from `user` user0_ where user0_.user_id=?
select user0_.user_id as user_id1_14_0_, user0_.created_at as created_2_14_0_, user0_.updated_at as updated_3_14_0_, user0_.average_rating as average_4_14_0_, user0_.code as code5_14_0_, user0_.email as email6_14_0_, user0_.grade as grade7_14_0_, user0_.nick_name as nick_nam8_14_0_, user0_.profile_photo as profile_9_14_0_, user0_.role as role10_14_0_, user0_.social_type as social_11_14_0_, user0_.review_counts as review_12_14_0_ from `user` user0_ where user0_.user_id=1;
2024-02-11 18:10:04.140 INFO 28784 --- [nio-8080-exec-5] p6spy : #1707642604140 | took 8ms | commit | connection 5| url jdbc:mysql://proddb-mysql.c1y4skgea7wb.ap-northeast-2.rds.amazonaws.com:3306/OTTifyDB
;
페이징 처리 성능 최적화를 통해 실행되는 쿼리의 개수가 9개에서 4개로 줄어든 것을 확인할 수 있다.
'Framework > JPA' 카테고리의 다른 글
[QueryDSL] DTO로 조회하기 (0) | 2023.10.15 |
---|---|
[JPA] 다대일 매핑에서 @JoinColumn의 역할 (0) | 2023.10.05 |
[JPA] @MappedSupperclass vs @Embedded & @Embeddable (0) | 2023.10.01 |
[JPA] 순한 참조 문제 해결하기 (0) | 2023.10.01 |