The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3.0 KiB
3.0 KiB
id, kind, slug, title, topic, topicName, project, status, studio, decisionStatus
| id | kind | slug | title | topic | topicName | project | status | studio | decisionStatus |
|---|---|---|---|---|---|---|---|---|---|
| 5e4d033c-d6fe-4257-a4dc-1ade44473c72 | PROJECT_DECISION | no-collection-fetch-join-with-pagination | Collection Fetch Join과 Pagination을 같이 사용하지 않는다 | jpa-feed-query-performance | 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에 페이징이 걸릴 때 실패시키는 설정을 켤 수 있다. 근본 해결은 아니지만 안전장치가 된다.
- 회귀 가드는 경고 코드 번호만 비교하지 않는다. 버전에 따라 코드가 달라질 수 있어 문구도 함께 확인한다.
- 작은 데이터셋으로만 검증하면 이 문제를 놓친다. 데이터 규모를 바꿔 가며 반환 크기와 로드 수를 함께 봐야 한다.