문제 상황
게시글을 조회할 때 관련된 좋아요, 해시태그, 카테고리 정보를 함께 조회하면서 페이징처리까지 하는 api가 있습니다.
이 때, 게시글의 입장에서 좋아요와 해시태그는 ~toMany 관계이고, 카테고리는 ~toOne의 관계입니다.
보통 N+1 문제를 피하기 위해서 연관된 엔티티를 함께 영속화 시키기 위해 lazy loading + fetch join을 사용하는데요, fetch join은 JPA의 Pageable과 함께 사용할 때 문제가 생깁니다.
✔️ 문제 쿼리
@Query(value = "select b from Board b " +
"join fetch b.category " +
"left join fetch b.likes " +
"left join fetch b.boardHashTags bt"
, countQuery = "select count(b.id) from Board b")
Page<Board> findAllBoards(Pageable pageable);
✔️ 실행결과
Hibernate:
select
b1_0.*
from
board b1_0
join
category c1_0
on c1_0.category_id=b1_0.category_id
left join
likes l1_0
on b1_0.board_id=l1_0.board_id
left join
boardHashTags b2_0
on b1_0.board_id=b2_0.board_id
hibernate 쿼리 로그를 살펴보면, 페이징 구문이 수행되지 않는 것을 확인할 수 있습니다.
org.hibernate.orm.query : HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory
또한 위와 같은 로그를 확인할 수 있습니다.
✔️ 문제 원인
Pageable을 사용하면 테이블을 FULL SCAN해서 애플리케이션 메모리에 올리고, 애플리케이션에서 요청한 페이지에 맞춰서 잘라내기 때문에 memory 오류가 발생합니다.
limit을 중복 row를 고려해서 걸어야할지 중복row를 무시하고 그냥 limit를 날려야할지 고민해야 하는데,
sql문으로 해결하지 않고 그냥 메모리에 데이터를 전부 올려버리고 페이지네이션하고 있기 때문입니다.
해결책
✔️ batch fetch size 설정
~toMany의 관계에 있는 엔티티들의 경우, fetch join을 사용하지 않는 대신에 batch fetch size 옵션을 통해 N+1문제를 해결할 수 있습니다.
이 옵션을 사용하면 설정한 사이즈 만큼 데이터를 끌어와서 컬랙션/프록시 객체를 한꺼번에 in 쿼리를 사용해 조회합니다. 설정한 크기 만큼 미리 조회하기 때문에 연관된 엔티티수만큼 참조객체를 조회하는 쿼리를 날리는 것이 아니라, 참조 객체를 in쿼리를 사용해 한 번에 불러오게 됩니다.
즉, 한 번만 쿼리를 더 날리면 되기 때문에 N+1에서 1+1로 개선할 수 있습니다.
jpa:
hibernate:
default_batch_fetch_size = 1000
✔️ fetch join
~toOne의 관계에 있는 엔티티들은 그대로 fetch join을 사용합니다.
결과
@Query(value = "SELECT b FROM Board b " +
"join fetch b.category " +
"left join b.likes " +
"left join b.boardHashTags",
countQuery = "select count(b.id) from Board b")
Page<Board> findAllBoards(Pageable pageable);
@Query(value = "SELECT b " +
"FROM Board b " +
"join b.category " +
"WHERE b.category.name = :categoryName")
Page<Board> findBoardsByCategoryName(Pageable pageable, String categoryName);
Hibernate:
select
b1_0.*
from
board b1_0
join
category c1_0
on c1_0.category_id=b1_0.category_id
order by
b1_0.board_id desc **limit ?,
?**
Hibernate:
select
l1_0.*
from
likes l1_0
where
l1_0.board_id **in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)**
Hibernate:
select
b2_0 .*
from
board_hash_tags b2_0
where
b2_0.board_id **in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)**
~ToMany 관계인 좋아요, 해시태그가 in 쿼리로 한 번 더 조회되는 것을 확인할 수 있습니다.
'프로젝트 Project' 카테고리의 다른 글
static 내부 클래스로 DTO 관리하기 (0) | 2024.08.06 |
---|---|
Spring Security + JWT로 로그인 구현하기 (1) (0) | 2024.08.05 |
멀티 모듈로 프로젝트 구성하기 (0) | 2024.08.05 |
Ehcache로 캐싱하기 (0) | 2024.08.02 |
인덱스 적용하기 (0) | 2024.08.02 |