Files
document-haness/docs/n+1liner/tech-log-studio/jpa-feed-query-performance/decision/decision-no-collection-fetch-join-with-pagination.md
T

3.0 KiB

id, kind, slug, title, topic, project, status, studio, decisionStatus
id kind slug title topic project status studio decisionStatus
5e4d033c-d6fe-4257-a4dc-1ade44473c72 PROJECT_DECISION no-collection-fetch-join-with-pagination Collection Fetch Join과 Pagination을 같이 사용하지 않는다 JPA 피드 조회 성능 Liner N + 1문제 게시 전 https://hyeonworks.com/studio/documents/5e4d033c-d6fe-4257-a4dc-1ade44473c72/edit PROPOSED

Collection Fetch Join과 Pagination을 같이 사용하지 않는다

컬렉션을 fetch join한 쿼리에 페이징을 걸지 않는다. Hibernate가 DB LIMIT을 빼고 결과셋 전체를 메모리에 올린 뒤 부모 기준으로 자르기 때문에, 응답은 한 페이지지만 비용은 데이터셋 전체에 비례한다.

근거

  • Collection Fetch Join Pagination의 In-memory Paging 이 동작을 실행계획과 로드 수로 확인한 기록이다.
  • Fetch Join으로 N+1을 해결하다 만난 MultiBag과 행 폭증 컬렉션 fetch join이 행을 곱하는 것을 확인한 기록이다.
  • Fetch Join · Batch · Projection 선택 기준 대신 무엇을 쓸지 정한 기준이다.

결정문

컬렉션을 fetch join하는 쿼리에 firstResult나 maxResults를 적용하지 않는다.

페이징이 필요한 목록 조회에서는 엔티티만 페이징해 DB LIMIT이 정상 발행되게 하고, 지연 연관은 배치나 별도 쿼리로 채운다.

판단 이유

컬렉션 fetch join에서는 부모 한 행이 자식 수만큼 늘어난다. 여기에 부모 기준 LIMIT을 걸면 조인 행에서 잘려 일부 부모의 자식이 누락된다.

Hibernate는 이 손상을 피하려고 SQL에서 LIMIT을 빼고 전체 조인 결과를 읽은 뒤 메모리에서 부모 기준으로 자른다. 발행된 SQL에 Limit 노드가 없는 것이 이 동작의 증거다.

측정에서 반환 목록은 페이지 크기로 고정됐지만 로드한 부모 엔티티는 데이터셋 전체였다. 초과 적재 배수는 데이터가 커질수록 늘었다. 작은 데이터셋에서는 두 값이 같아 문제가 드러나지 않는다.

엔티티만 페이징하면 Limit 노드가 정렬 위에 얹혀 상위 몇 행만 취하는 정렬로 바뀐다. 전체 정렬과 상위 몇 행 정렬의 차이가 계획 수준에서 나타난다.

영향

  • 컬렉션을 한 번에 가져오는 편의를 포기한다. 자식 조회를 위한 쿼리가 따로 필요하다.
  • 엔티티만 페이징하면 지연 연관의 N+1이 돌아온다. 배치나 프로젝션을 함께 적용해야 한다.
  • 이 실수를 조기에 발견하려면 컬렉션 fetch join에 페이징이 걸릴 때 실패시키는 설정을 켤 수 있다. 근본 해결은 아니지만 안전장치가 된다.
  • 회귀 가드는 경고 코드 번호만 비교하지 않는다. 버전에 따라 코드가 달라질 수 있어 문구도 함께 확인한다.
  • 작은 데이터셋으로만 검증하면 이 문제를 놓친다. 데이터 규모를 바꿔 가며 반환 크기와 로드 수를 함께 봐야 한다.