Files
llm-wiki/raw/errors/hibernate-getcollectionfetchcount-batch-semantics-2026-07-13.md

4.2 KiB

title, source_type, status, related_branches, related_projects, tags, created, status_label
title source_type status related_branches related_projects tags created status_label
error / hibernate-getcollectionfetchcount-batch-semantics-2026-07-13 error-note raw
experiment-nplus1-highlight-feed
ca-skeleton
error
ca-skeleton
hibernate
statistics
batch-fetch
n-plus-one
metric-semantics
resolved
2026-07-13 resolved

error: hibernate-getcollectionfetchcount-batch-semantics-2026-07-13

Layer: raw/errors/ — 작업 중 마주친 지표 의미 오해(측정 정정) 기록.

Parent / 부모

증상 / Symptom

  • 문서 모델(L4 가이드 §0.4, 발표 문서 §6.1): "Statistics.getCollectionFetchCount() = 초기화된 컬렉션 수라 배치를 켜도 그대로 N, 변하는 건 SQL 수(getPrepareStatementCount)뿐".
  • 실측(FeedBatchFetchIT, default_batch_fetch_size=100): getCollectionFetchCount()가 L1(배치 없음)의 N(10/100/1000)에서 L5(배치)의 **1 / 1 / 10 = ceil(N/batch)**로 떨어졌다.
  • 즉 이 지표는 "초기화된 컬렉션 수"가 아니라 컬렉션을 채운 fetch SELECT 연산 수다. 만약 회귀가드를 "배치를 켜도 collectionFetch는 N으로 그대로"라는 전제로 짰다면 거짓 실패했을 것.
  • 발생 환경: Java 21 · Spring Boot 4.0.0 · Hibernate ORM 7.1.8 · PostgreSQL 16(Testcontainers).
  • 재현 가능 여부: always.

재현 절차 / Reproduction

  1. FeedBatchFetchIT@TestPropertySource(… hibernate.default_batch_fetch_size=100)를 얹고 queryAdapter.loadFeed(0, n)(N=10/100/1000) 호출.
  2. stats.getCollectionFetchCount() 관측 → 1 / 1 / 10.
  3. 대조: FeedPersistenceIT(배치 없음)의 L1 l1CollectionNPlusOneGrowsLinearlyWithN에서 같은 지표 = N(10/100/1000).
  4. 결론: 배치가 컬렉션 fetch 연산을 ceil(N/batch)로 접는다 — 지표는 초기화 수가 아니라 fetch 연산 수.

근본 원인 / Root cause

  • 직접 원인: getCollectionFetchCount()의 이름을 "초기화된 컬렉션 수"로 가정했으나, 실제 집계 단위는 컬렉션을 채운 fetch(SELECT) 연산 횟수다. 배치 페치는 여러 컬렉션을 한 SELECT로 채우므로 이 카운트가 준다.
  • 근본 원인: Hibernate Statistics의 카운터 이름을 문서 없이 "직관적 의미"로 가정. L1에서는 배치가 없어 "초기화 수 N = fetch 연산 N"이 우연히 일치해 오해가 드러나지 않았다.
  • 트리거 조건: 배치 페치(default_batch_fetch_size 또는 @BatchSize)를 켠 뒤 컬렉션 다수를 초기화.

Sources / 근거

  • 로컬 실측: ca-tmpl:app-bootstrap FeedBatchFetchIT.l5BatchFetchCollapsesQueryCountcollectionInit=1/1/10 관찰(build/lab-results/feed-nplus1-l5.md). 대조 FeedPersistenceIT L1 = N. 둘 다 :app-bootstrap:test GREEN.
  • 문서: ca-tmpl:docs/notes/L5.md §"실측 정정" + 발표 문서 topic-arrange/n+1liner/n+1liner.md §11.2 "⚠ 실측 정정" 콜아웃.
  • 적용: 문서 모델을 정정 — "getCollectionFetchCount() = 컬렉션 fetch SELECT 연산 수(배치에서 ceil(N/batch)로 접힘)". 배치 해결의 증인은 preparedcollectionFetch 둘 다.
  • 회귀가드는 "배치를 켜도 collectionFetch가 N으로 유지"라는 전제를 쓰지 않는다. 대신 prepared < N(붕괴) 또는 feedItemLoaded == min(pageSize, N)(페이징 정상) 같은 배치-불변 단언을 쓴다.

교훈 / Lesson

  • ORM 통계 카운터는 이름의 직관과 집계 단위가 다를 수 있다. getCollectionFetchCount/getEntityFetchCount 등은 "초기화된 개수"가 아니라 "fetch 연산(SELECT) 수"에 가깝다 — 배치/서브셀렉트를 켜면 그 값이 준다. 지표를 회귀가드로 쓰기 전에 대조 실측(배치 on/off)으로 의미를 못 박아라.
  • 같은 결의 정정이 이 랩에 셋: L3 "Hibernate 6+ 루트 dedup"(리스트 크기≠전송 행수), L4 "HHH000104HHH90003004"(로그 코드 드리프트), L5 여기(collectionFetch=fetch 연산 수). ORM 버전·설정이 관측 지표를 바꾸므로 실측으로 재확인이 원칙.