--- 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 버전이 관측 지표를 바꾸므로 **실측으로 재확인**해야 한다.