사례 §
- Order 100건을 조회한 뒤
order.getMember().getName()을 찍었더니 쿼리가 101번 나갔다.
- Team 20건을 페이징으로 조회한 뒤 각 team의 members를 화면에 뿌리려다가 N+1이 발생했다.
- 주문 목록 API에서
select order 1번 뒤에 select member where id=?가 주문 수만큼 반복됐다.
- 화면에는
orderItems도 같이 보여줘야 했고, 페이징은 필수였다.
왜 N+1이 생겼는지 §
- N+1은 부모 엔티티 1개를 조회한 뒤, 각 부모의 연관 엔티티를 조회하는 순간 추가 쿼리가 n번 발생해 총 n+1번이 되는 현상이다.
- lazy loading에서 주로 발생하지만, eager도 조회 방식에 따라 불필요한 쿼리가 생길 수 있다.
- 주문 1회 조회 이후 member 조회가 n번 반복되는 구조가 문제였다.
orderItem은 oneToMany 관계라 조회 방식에 따라 영향이 더 커졌다.
해결 선택지 §
- ToOne N+1은
fetch join으로 보통 해결 가능하고 안전했다.
- 같은 구간에서
EntityGraph나 batch size를 주는 방법도 쓸 수 있었다.
Batch Fetching(@BatchSize, hibernate.default_batch_fetch_size)를 적용하는 선택지도 있었다.
- 필요한 모양으로 한 번에 조회하는 DTO 프로젝션도 가능했다.
페이징과 fetch join 충돌 지점 §
- 페이징 구간에서
fetch join을 쓰면 row가 부모*자식만큼 늘어날 수 있다.
- 그래서 중복이나 메모리, 페이징 이슈가 같이 발생할 수 있다.
- 이 경우
batch size를 쓰거나, 부모에서 memberIds를 조회한 뒤 IN 절로 다시 조회하는 방식으로 풀 수 있다.
정리 §
- 문제의 핵심은 부모 조회 1번 뒤에 연관 조회가 n번 붙는 구조였다.
- ToOne과 ToMany를 같은 방식으로 처리하면 페이징에서 바로 부딪힌다.
- 조회 구간별로
fetch join, EntityGraph, batch, IN 재조회 중에서 선택해서 가져가야 한다.