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>
4.8 KiB
id, kind, slug, title, topic, topicName, project, status, studio
| id | kind | slug | title | topic | topicName | project | status | studio |
|---|---|---|---|---|---|---|---|---|
| db99cbc5-9123-4599-b368-39ff3170e81d | REFERENCE | fetch-strategy-selection | Fetch Join · Batch · Projection 선택 기준 | jpa-feed-query-performance | JPA 피드 조회 성능 | Liner N + 1문제 | 게시 전 | https://hyeonworks.com/studio/documents/db99cbc5-9123-4599-b368-39ff3170e81d/edit |
Fetch Join · Batch · Projection 선택 기준
세 전략은 서로 다른 비용을 줄인다. fetch join은 왕복을 접지만 행을 곱하고, batch는 왕복을 묶지만 엔티티를 그대로 만들고, 프로젝션은 적재를 없애지만 행수를 줄이지 않는다.
관계
- Fetch Join으로 N+1을 해결하다 만난 MultiBag과 행 폭증 fetch join의 한계를 확인한 기록이다.
- Collection Fetch Join Pagination의 In-memory Paging fetch join과 페이징이 함께 서지 못하는 것을 확인한 기록이다.
- Projection 이후에도 1,509행을 읽은 Row Over-fetch 프로젝션이 남기는 비용을 확인한 기록이다.
목적
쿼리 수만 보고 전략을 고르면 비용이 다른 축으로 옮겨 간 것을 놓친다. 컬렉션 fetch join은 쿼리 수를 크게 줄이면서 전송 행수와 메모리를 키운다.
무엇을 줄이려는지 먼저 정하고 그 축을 재는 지표로 전후를 비교한다.
규칙
1. 컬렉션 fetch join은 두 개 이상 쓰지 않는다
순서 컬럼이 없는 List 두 개를 동시에 fetch join하면 곱집합을 원래 컬렉션으로 되돌릴 수 없어 쿼리 생성 시점에 거부된다. 데이터가 0건이어도 발생하는 매핑 단계의 거부다.
2. 컬렉션 fetch join은 행을 곱한다
컬렉션 하나만 fetch join해도 부모 한 행이 자식 수만큼 반복된다. 전송 행수는 자식 총합이 된다.
Hibernate 6 이상은 루트 엔티티를 자동으로 중복 제거하므로 결과 리스트 크기로는 이 증가가 보이지 않는다. 조인 카디널리티나 실행계획의 actual rows로 확인한다.
3. 컬렉션 fetch join과 페이징을 같이 쓰지 않는다
부모 기준 LIMIT을 걸면 조인 행에서 잘려 일부 부모의 자식이 누락된다. Hibernate는 이를 피하려고 SQL에서 LIMIT을 빼고 전체를 읽은 뒤 메모리에서 자른다.
응답은 한 페이지지만 로드한 부모는 전체다. 발행 SQL에 Limit 노드가 없는 것이 이 동작의 증거다.
4. fetch join은 ToOne에 쓴다
ToOne은 행을 곱하지 않는다. 루트 SQL에 합쳐도 카테시안이 생기지 않으므로 fetch join이 적합하다.
5. 컬렉션에는 batch fetch를 쓴다
엔티티만 페이징해 DB LIMIT이 정상 작동하게 한 뒤, 지연 연관은 부모 키를 모아 IN으로 채운다. 배치 크기가 B면 왕복은 부모 수를 B로 나눈 올림값이 된다.
배치는 부모와 자식을 곱하지 않는다. 실행계획에서 semi-join으로 나타난다.
6. 화면 조회에는 프로젝션을 쓴다
필요한 스칼라 값만 조회하면 영속 엔티티를 만들지 않는다. 1차 캐시, 더티체킹, 지연 프록시도 생기지 않는다.
join이 있어도 컬럼을 읽기 위한 경로일 뿐 엔티티를 만들지 않는다. 이 동작은 배치 설정 여부와 관계없이 성립한다.
7. 프로젝션의 효과는 실행계획이 아니라 ORM 층에서 확인한다
필요한 컬럼만 골라도 EXPLAIN의 width가 줄지 않을 수 있다. 조인 대상의 행폭이 반영되고 width가 실제 전송 바이트가 아니라 타입의 평균폭 추정치이기 때문이다.
프로젝션의 이득은 엔티티 로드 수로 확인한다.
8. 세 전략이 남기는 비용을 적는다
fetch join은 행 폭증과 페이징 불가를 남긴다. batch는 엔티티 과적재를 남긴다. 프로젝션은 부모당 자식 전량 조회를 남긴다.
남은 비용을 적어야 다음 단계가 무엇을 풀어야 하는지 이어진다.
적용 조건
- 연관을 포함한 목록 조회를 설계할 때
- N+1을 확인하고 fetch 전략을 고를 때
- 전략을 바꾼 뒤 무엇이 줄고 무엇이 남았는지 정리할 때
예외
- 컬렉션이 하나이고 페이징이 없으며 자식 수가 작다면 컬렉션 fetch join이 단순하다. 자식 수가 커질 수 있는 구조에는 쓰지 않는다.
- 배치 크기 설정은 세션 전체에 영향을 준다. 기존 측정을 유지하려면 별도 설정 범위로 격리한다.
예시
- 컬렉션 두 개 fetch join : 쿼리 생성 시점 거부
- 컬렉션 한 개 fetch join : 전송 행수 = 자식 총합
- 컬렉션 fetch join + 페이징 : DB LIMIT 없음, 부모 전체 로드
- ToOne fetch join : 행 곱하지 않음, 적합
- batch fetch : 왕복 = 부모 수 / 배치 크기 올림
- 프로젝션 : 엔티티 로드 0, 쿼리 상수, 자식 행수는 그대로