Files
document-haness/docs/n+1liner/tech-log-studio/jpa-feed-query-performance/reference/reference-nplus1-quantitative-diagnosis.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
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>
2026-09-04 22:51:59 +09:00

5.2 KiB
Raw Blame History

id, kind, slug, title, topic, topicName, project, status, studio
id kind slug title topic topicName project status studio
b0b55ac9-c0a3-4c01-ba84-0aa478923ace REFERENCE nplus1-quantitative-diagnosis JPA N+1 정량 진단 기준 jpa-feed-query-performance JPA 피드 조회 성능 Liner N + 1문제 게시 전 https://hyeonworks.com/studio/documents/b0b55ac9-c0a3-4c01-ba84-0aa478923ace/edit

JPA N+1 정량 진단 기준

N+1을 쿼리 로그의 인상이 아니라 지표로 확인한다. Hibernate Statistics의 지표는 이름이 뜻하는 것이 서로 달라서, SQL 실행 횟수로 바꿔 읽으면 배치를 적용한 뒤 결론이 어긋난다.

관계

  • Fetch 타입이 아니라 조회 방식이 만든 ToOne N+1 엔티티별 fetch 통계로 ToOne 쪽을 확인한 기록이다.
  • PostgreSQL Query Plan 측정 기준 같은 측정에서 실행계획을 다루는 기준이다.

목적

쿼리가 몇 개 나갔는지만 세면 어느 연관이 문제인지 알 수 없다. 총계에는 목록 루트, 페이지 count, ToOne 2차 SELECT, 컬렉션 초기화가 섞여 있다.

지표를 나눠 읽고 총계를 항등식으로 검산하면 어느 연관이 몇 번 조회되는지 확정할 수 있다. 그래야 fetch 전략을 바꿨을 때 무엇이 줄었는지 말할 수 있다.

규칙

1. 지표 이름이 뜻하는 것을 그대로 읽는다

getCollectionFetchCount()는 초기화된 컬렉션 수다. 실행된 SELECT SQL 수가 아니다.

getPrepareStatementCount()는 획득한 PreparedStatement 수다. 이 값도 SQL 실행 수와 항상 같지는 않다.

getEntityFetchCount()는 2차 fetch로 초기화된 엔티티 수다. 실행된 SELECT SQL 수가 아니다.

2. 등식이 성립하는 조건을 함께 적는다

batch나 subselect가 없을 때만 초기화 컬렉션 수와 자식 SELECT 수가 같다. 이 조건에서만 컬렉션 수를 SQL 수로 바꿔 읽을 수 있다.

Batch Fetch를 적용하면 여러 컬렉션을 한 SQL로 채우므로 등식이 깨진다. 배치 적용 여부는 prepared와 collectionFetch를 함께 보고 판단한다.

3. 총계를 형태별로 가르고 검산한다

총 PreparedStatement를 다음처럼 나눈다.

content 1 count 1 distinct ToOne 대상 수 N ToOne (아이템마다 다른 연관) N 컬렉션 초기화

파생값과 직접 측정값이 일치하는지 교차 검증한다. 회계 항등식은 총 PreparedStatement에서 컬렉션 N, content 1, count 1을 뺀 값이 entityFetch와 같은지 보는 것이다.

4. 회귀 가드는 시더 카디널리티와 무관한 값으로 고정한다

합계 지표는 Hibernate 버전에 따라 집계 범위가 달라질 수 있다. 엔티티별 지표로 고정하는 편이 안정적이다. 예를 들어 아이템마다 다른 연관은 pageFetch == N이 성립한다.

합계는 회귀 가드가 아니라 교차 검증에 쓴다.

5. count 쿼리가 언제 나오는지 안다

Page를 반환하면 Spring Data가 전체 건수 count를 한 번 더 실행한다. offset이 0이고 pageSize가 반환 건수보다 크면 count를 건너뛴다.

같은 코드라도 pageSize와 반환 건수의 관계에 따라 총계가 달라진다. 측정값을 비교할 때 이 조건을 맞춘다.

6. 캐시가 결과를 먹지 않게 한다

같은 트랜잭션에서 조회를 반복하면 1차 캐시가 쿼리를 흡수한다. 지연 반복 루프는 매 반복마다 타이머를 켜기 전에 em.clear()를 호출한다. clear 비용은 측정 구간 밖에 둔다.

쿼리 수는 stats.clear() 직후 1회 실행분으로만 읽어 회당 정확값을 얻는다.

7. 증가 기준이 무엇인지 명시한다

N+1의 N은 전체 테이블 크기가 아니라 한 요청에서 조립하는 부모 엔티티 수다. 테이블이 100만 행이어도 이 왕복 수는 늘지 않는다.

전체 테이블 크기는 OFFSET, 정렬, 가시성 필터 비용에 영향을 준다. 이 비용은 별도 축으로 분리해 측정한다.

8. 왕복과 행수를 다른 축으로 센다

한 조회에 두 위반이 함께 있을 수 있다. 부모 수에 비례하는 왕복과, 한 번의 왕복에서 자식을 전부 읽는 과조회다.

왕복은 fetch 전략으로, 행수는 SQL 형태와 인덱스로 푼다. 한쪽을 고쳐 놓고 다른 쪽이 해결됐다고 적지 않는다.

적용 조건

  • ORM 조회에서 쿼리 발생량이 데이터 규모를 따라 늘어나는지 확인할 때
  • fetch 전략을 바꾸고 전후를 같은 지표로 비교할 때
  • N+1 회귀를 테스트로 고정할 때

예외

  • SQL 형태별 정확한 실행 횟수가 필요하면 이 지표만으로 부족하다. SQL 로그, StatementInspector, datasource-proxy, p6spy, PostgreSQL statement logging 중 하나로 따로 수집한다.
  • 운영 종단 지연이나 처리량이 필요하면 이 측정의 범위 밖이다. 부하 테스트와 APM으로 확인한다.

예시

  • 초기화 컬렉션 수 : 실행된 SELECT 수가 아니라 초기화된 컬렉션 수
  • 총 PreparedStatement : 획득한 statement 수, SQL 실행 수와 다를 수 있음
  • 회계 항등식 : 총계 − 컬렉션 N content 1 count 1 = entityFetch
  • 회귀 가드 : pageFetch == N (엔티티별, 시더 카디널리티 무관)
  • 측정 규율 : 매 반복 전 em.clear, stats.clear 직후 1회만 읽기