4.2 KiB
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 |
|
|
|
2026-07-13 | resolved |
error: hibernate-getcollectionfetchcount-batch-semantics-2026-07-13
Layer:
raw/errors/— 작업 중 마주친 지표 의미 오해(측정 정정) 기록.
Parent / 부모
- raw/branch-notes/experiment-nplus1-highlight-feed — N+1 랩 L5(엔티티 페이징 + 배치 페치) 실행 중
getCollectionFetchCount()의 실제 의미가 문서 모델과 달라 발견.
증상 / 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
FeedBatchFetchIT에@TestPropertySource(… hibernate.default_batch_fetch_size=100)를 얹고queryAdapter.loadFeed(0, n)(N=10/100/1000) 호출.stats.getCollectionFetchCount()관측 → 1 / 1 / 10.- 대조:
FeedPersistenceIT(배치 없음)의 L1l1CollectionNPlusOneGrowsLinearlyWithN에서 같은 지표 = N(10/100/1000). - 결론: 배치가 컬렉션 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-bootstrapFeedBatchFetchIT.l5BatchFetchCollapsesQueryCount—collectionInit=1/1/10관찰(build/lab-results/feed-nplus1-l5.md). 대조FeedPersistenceITL1 = N. 둘 다:app-bootstrap:testGREEN. - 문서:
ca-tmpl:docs/notes/L5.md§"실측 정정" + 발표 문서topic-arrange/n+1liner/n+1liner.md§11.2 "⚠ 실측 정정" 콜아웃.
권고 해결 / Recommended resolution (적용됨)
- 적용: 문서 모델을 정정 — "
getCollectionFetchCount()= 컬렉션 fetch SELECT 연산 수(배치에서ceil(N/batch)로 접힘)". 배치 해결의 증인은prepared와collectionFetch둘 다. - 회귀가드는 "배치를 켜도 collectionFetch가 N으로 유지"라는 전제를 쓰지 않는다. 대신
prepared < N(붕괴) 또는feedItemLoaded == min(pageSize, N)(페이징 정상) 같은 배치-불변 단언을 쓴다.
교훈 / Lesson
- ORM 통계 카운터는 이름의 직관과 집계 단위가 다를 수 있다.
getCollectionFetchCount/getEntityFetchCount등은 "초기화된 개수"가 아니라 "fetch 연산(SELECT) 수"에 가깝다 — 배치/서브셀렉트를 켜면 그 값이 준다. 지표를 회귀가드로 쓰기 전에 대조 실측(배치 on/off)으로 의미를 못 박아라. - 같은 결의 정정이 이 랩에 셋: L3 "Hibernate 6+ 루트 dedup"(리스트 크기≠전송 행수), L4 "
HHH000104→HHH90003004"(로그 코드 드리프트), L5 여기(collectionFetch=fetch 연산 수). ORM 버전·설정이 관측 지표를 바꾸므로 실측으로 재확인이 원칙.