Files
llm-wiki/raw/errors/hibernate7-hhh90003004-collection-fetch-paging-2026-07-13.md

60 lines
5.0 KiB
Markdown

---
title: error / hibernate7-hhh90003004-collection-fetch-paging-2026-07-13
source_type: error-note
status: raw
related_branches: [experiment-nplus1-highlight-feed]
related_projects: [ca-skeleton]
tags: [error, ca-skeleton, hibernate, hibernate7, n-plus-one, collection-fetch, pagination, log-code-drift, resolved]
created: 2026-07-13
status_label: resolved
---
# error: hibernate7-hhh90003004-collection-fetch-paging-2026-07-13
> Layer: `raw/errors/` — 작업 중 마주친 단일 실패·놀라움 기록. 원본은 raw에 영구 보관한다.
## Parent / 부모
- [[raw/branch-notes/experiment-nplus1-highlight-feed]] — N+1 랩 L4(컬렉션 fetch join + 페이징 → 인메모리 페이징) 실행 중 경고 캡처 테스트에서 발견.
## 증상 / Symptom
- 기대: 컬렉션 fetch join에 페이징(`setMaxResults`)을 걸면 널리 알려진 경고 코드 **`HHH000104`**(`firstResult/maxResults specified with collection fetch; applying in memory`)가 WARN으로 찍힌다.
- 실제(Logback `ListAppender``org.hibernate` WARN 캡처, 원문 그대로):
```text
HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory
```
- **코드 번호가 다르다**: 문서·다수 블로그가 말하는 `HHH000104`가 아니라 `HHH90003004`. **메시지 본문 문구는 동일**.
- 파급: 회귀가드를 `assertThat(warnings).anyMatch(m -> m.contains("HHH000104"))`처럼 **코드 번호만으로** 매칭했다면 이 테스트는 **거짓 실패**했을 것이다. 실제로는 `|| m.contains("collection fetch")` 분기가 어서션을 통과시켰다.
- 발생 환경: Java 21 · Spring Boot 4.0.0 · Hibernate ORM 7.1.8.Final · PostgreSQL 16(Testcontainers).
- 재현 가능 여부: `always`.
## 재현 절차 / Reproduction
1. `ca-tmpl`에서 `FeedPersistenceIT`(app-bootstrap)의 `l4EmitsHhh000104InMemoryPagingWarning` 테스트를 둔다: `org.hibernate` 로거에 `ListAppender`를 붙이고, `select f from FeedItemJpaEntity f join fetch f.highlights order by ...`에 `setFirstResult(0).setMaxResults(20)`를 걸어 `getResultList()` 실행.
2. `cd src && ./gradlew :app-bootstrap:test --tests '*FeedPersistenceIT'` (Docker 필요 — Testcontainers).
3. `LabReport.observe("L4 HHH000104 warning (verbatim)", ...)`가 남긴 원문 확인 → `HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory`.
4. 결과: 테스트 GREEN(0 fail) — 단, 코드 번호로만 매칭했다면 red였을 것.
## 근본 원인 / Root cause
- 직접 원인: Hibernate ORM이 이 경고의 **메시지 코드를 6→7 사이에 재부여**했다. `HHH000104`(구) → `HHH90003004`(현). 메시지 본문(`firstResult/maxResults specified with collection fetch; applying in memory`)과 의미(컬렉션 fetch join + 페이징 = DB `LIMIT` 없이 결과셋 전체를 메모리로 올려 인메모리 페이징)는 그대로다.
- 근본 원인: 로그 메시지 코드는 **버전 간 안정 계약이 아니다**. 널리 인용되는 코드 번호(`HHH000104`)를 버전 불변 상수로 취급하면 상위 버전에서 매칭이 깨진다.
- 트리거 조건: Hibernate 7.x에서 컬렉션 fetch join + `setMaxResults`/`setFirstResult`(기본 `hibernate.query.fail_on_pagination_over_collection_fetch=false`).
## Sources / 근거
- 로컬 실측: `ca-tmpl:app-bootstrap` `FeedPersistenceIT.l4EmitsHhh000104InMemoryPagingWarning` — `ListAppender`가 캡처한 WARN 원문이 `HHH90003004: ...`. `build/lab-results/feed-nplus1.md`의 "L4 HHH000104 warning (verbatim)" 관찰 블록에 원문 적재. `:app-bootstrap:test --tests '*FeedPersistenceIT'` GREEN(0 fail).
- 런타임: `docs/notes/L4.md` §"실측 정정" + 발표 문서 `topic-arrange/n+1liner/n+1liner.md` §10 "⚠ 측정 정정" 콜아웃에 동일 원문 기록.
## 권고 해결 / Recommended resolution (적용됨)
- 적용: 경고 매칭을 **코드 번호가 아니라 메시지 문구**로도 하도록 `m.contains("HHH000104") || m.contains("collection fetch")` OR 매칭. Hibernate 버전이 코드를 다시 바꿔도(또는 카테고리/문구가 흔들려도) 견고.
- 대안: 특정 버전에 고정하려면 실행 시 캡처한 원문을 먼저 확인해 정확한 현재 코드(`HHH90003004`)로 좁힐 수 있으나, 상위 버전 이식성을 잃는다 → 랩에서는 문구 매칭을 채택.
## 교훈 / Lesson
- **Hibernate 로그 메시지 코드는 버전 불변 계약이 아니다.** `HHH######` 번호로 로그를 assert하면 상위 버전에서 조용히 깨진다 — **메시지 문구(의미를 담은 부분)로 매칭**하는 편이 견고하다.
- 로그 기반 테스트는 **첫 실행에서 캡처한 원문을 반드시 확인**하고(여기선 `LabReport.observe`), 매칭 조건을 그 원문에 맞춰 좁히거나(문구) 넓게(OR) 둔다. "널리 알려진 코드"를 상수로 하드코딩하지 않는다.
- 같은 결의 정정이 이 랩에 하나 더 있다: L3의 "Hibernate 6+ 루트 자동 dedup"(fetch join 결과 리스트 크기 = Σ가 아니라 N) — ORM 버전이 관측 지표를 바꾸므로 **실측으로 재확인**해야 한다.