Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/runtime-contract-correctness/case/case-a05-f005-jparetrypolicy.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

9.8 KiB

kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, body, assets, evidence, source
kind slug title topic project status sourceRevision rootTreeNode evidenceCapturedOn body assets evidence source
CASE a05-f005-jparetrypolicy 애플리케이션이 넣은 재시도 정책이 한 번도 불리지 않고 작업만 두 번 돈다 runtime-contract-correctness clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:a05-f005-jparetrypolicy 2026-09-02 case-a05-f005-jparetrypolicy.body.md
key file
a05-f005-jparetrypolicy ../../../final/evidence/rendered/a05-f005-jparetrypolicy.svg
key file
a05-f005-jparetrypolicy-probe ../../../final/evidence/rendered/a05-f005-jparetrypolicy-probe.svg
../../../final/evidence/raw/a05-f005-jparetrypolicy.txt
../../../final/evidence/raw/a05-f005-jparetrypolicy-probe.txt
분석 문서는 persistence-jpa 편 §20 이다. 이것을 단순한 죽은 필드가 아니라 공개 조립 팩토리가 제공하는 기능이 동작하지 않는 계약 결함으로 규정하고 P2 로 둔다. §7.6 이 API 스코프에서 먼저 도달 불가 후보로 올리고 트랜잭션 스코프로 넘긴 흔적이 남아 있다.

애플리케이션이 넣은 재시도 정책이 한 번도 불리지 않고 작업만 두 번 돈다

조립 팩토리가 애플리케이션이 제공한 재시도 정책을 받는 오버로드를 공개한다. 그 정책은 대체 정책 필드에 담기고, 그 필드를 읽는 분기는 호출자 프로파일의 재시도 프로파일이 널일 때만 열린다. 프로파일 타입이 그것을 널로 두지 못하게 한다.

관계

  • 재시도 구현이 둘이고 정교한 쪽을 아무도 호출하지 않는다 같은 코디네이터의 다른 공백이다.
  • 재시도 단위는 statement가 아니라 유스케이스 전체다 이 정책이 참여하는 결정이다.
  • @Bean이 있다는 것은 조립 증거가 아니다 빈으로 등록된 확장점이라도 실제로 불리는지는 따로 세어 봐야 한다.

문제

트랜잭션 자동설정 팩토리는 오버로드를 둘 갖는다. 정책을 스스로 만드는 쪽과 밖에서 받는 쪽이다.

결론

정책 오버로드는 실제로 매번 불린다. 프로파일 오버로드가 하는 일이 그 호출 한 줄뿐이라서다. 그 자리를 채우는 호출자가 저장소 어디에도 없다.

넣으면 어떻게 되는지 실행으로 확인했다. 언제나 재시도 금지를 돌려주는 정책과 호출 카운터를 넣고, 두 번까지 허용하는 유효한 프로파일로 직렬화 실패를 던지는 작업을 돌렸다. 정책은 0 번, 작업은 2 번 불렸다. 정책은 말할 기회를 얻지 못했고 작업은 두 번 실행됐다.

그 정책이 담기는 필드의 javadoc 이 사연을 적는다. 예전 구현은 시도 횟수만 호출자에게서 받고 적격성과 백오프는 이 정책으로 덮었다. 재시도 하나를 프로파일 둘이 나눠 결정하게 되면서, 어느 쪽을 읽어도 실제 동작을 못 맞히게 됐다. 현재 형태가 그 수정의 결과다.

같은 javadoc 이 이 필드를 호출이 자기 재시도 프로파일을 주지 않을 때만 쓰인다고 적는다. 프로파일 타입의 컴팩트 생성자가 그 필드에 널 아님을 요구하므로, 문서에 남은 발동 조건이 코드에서는 성립하지 않는다.

앞쪽 오버로드의 javadoc 에는 두 값을 따로 두면 서로 어긋난 조합이 만들어지고 운영자 눈에는 그것이 설정 실수가 아니라 고장으로 보이기 때문에, 정책과 예산을 같은 프로파일에서 만든다고 적혀 있다. 그 문단 바로 아래에 정책만 따로 받는 오버로드가 놓여 있다.

플랫폼 자신도 같은 문에 걸린다. 런타임 자동설정이 시도 3회짜리 기본 프로파일로 정책을 만들어 넘기고, 그 정책은 대체 정책 필드에 담긴 뒤 읽히지 않는다.

예외도 경고도 없다. 코디네이터는 정책을 받았는지 널 검사만 하고, 그 정책이 쓰이지 않는다는 것은 어디서도 드러나지 않는다.

검증 환경

OpenJDK : 21.0.12 확인 방식 : 두 오버로드와 분기 열람, 애플리케이션 정책을 넣은 코디네이터 실행, 호출 계수 소스 수정 : x

재현 조건

  1. 자동설정의 두 오버로드를 읽는다. 앞의 것이 뒤의 것을 부르는지 확인한다.
  2. 뒤의 오버로드가 넘긴 정책이 담기는 필드와, 그 필드를 읽는 분기를 찾는다.
  3. 프로파일 타입의 컴팩트 생성자가 그 분기의 조건을 허용하는지 확인한다.
  4. 호출 카운터를 단 정책과 유효한 프로파일로 코디네이터를 세워 실패하는 작업을 실행한다.
  5. 정책 호출과 작업 호출을 센다.

본문

트랜잭션 자동설정 팩토리는 코디네이터를 만드는 오버로드를 둘 갖는다. 하나는 재시도 프로파일을 받고, 다른 하나는 애플리케이션이 준 정책을 받는다.

그 정책은 대체 정책 필드로 간다

:::evidence key="a05-f005-jparetrypolicy" alt="자동설정의 두 조립 팩토리와 앞쪽의 설계 근거 javadoc, 뒤쪽 오버로드가 넘긴 정책이 담기는 필드와 그 필드의 javadoc, 그 필드를 읽는 유일한 분기, 프로파일 타입의 컴팩트 생성자, 그리고 조립 팩토리 호출 전수와 코디네이터를 직접 생성하는 곳이 넘기는 정책을 출력한 터미널 기록." caption="두 오버로드와 앞쪽의 설계 근거 · 정책이 담기는 대체 정책 필드와 그 사연 · 분기는 널일 때만 열림 · 프로파일 생성자가 널을 금지 · 밖에서 넣은 정책 0 — 55줄 · exit 0" zoom="true" :::

앞쪽 오버로드의 javadoc 이 이유를 적는다. 정책과 예산은 같은 프로파일에서 만든다, 일부러 그렇게 한다. 따로 설정하면 재시도하라는 정책과 한 번만 허용하는 예산이 만나고, 그것은 오설정이 아니라 재시도가 고장 난 것처럼 보인다는 것이다.

바로 그 아래에 정책만 따로 받는 오버로드가 있다. 그 오버로드가 넘긴 정책은 fallbackRetryPolicy 필드에 담긴다.

그 필드를 읽는 자리는 하나다.

JpaRetryPolicy retryPolicy =
    profile.retryProfile() == null
        ? fallbackRetryPolicy
        : DefaultJpaRetryPolicy.forProfile(profile.retryProfile());

그리고 프로파일 타입의 컴팩트 생성자가 retryProfile 에 널 아님을 요구한다. 왼쪽 가지가 열리는 조건을 타입이 금지한다.

그 필드는 잊힌 것이 아니라 강등된 것이다

필드 javadoc 이 사연을 적는다. 코디네이터는 예전에 이 정책의 적격성과 백오프를 모든 호출에 적용하면서 시도 횟수만 호출자 프로파일에서 가져왔다. 프로파일 둘이 재시도 하나를 나눠 결정했고, 어느 쪽을 읽든 실제 동작을 잘못 예측하게 됐다.

지금 형태는 그 수정의 결과다. 같은 javadoc 이 이 필드를 호출이 자기 재시도 프로파일을 주지 않을 때만 쓰인다고 적는데, 그 조건이 곧 타입이 금지하는 것이다. 발동 조건이 문서에 남고 코드에서 사라졌다.

그 자리에 애플리케이션 정책이 들어온 적이 없다

정책 오버로드 자체는 매번 불린다. 프로파일 오버로드의 몸통이 그것을 부르는 한 줄이다.

밖에서 자기 정책을 넣는 호출은 프로덕션에도 테스트에도 없다. 유일한 프로덕션 진입은 프로파일 쪽이고, 테스트는 코디네이터를 직접 세우면서도 프로파일에서 만든 기본 정책만 넘긴다.

넣으면 어떻게 되는지 실행으로 확인했다

:::evidence key="a05-f005-jparetrypolicy-probe" alt="애플리케이션이 준 정책과 호출 카운터, 두 번까지 허용하는 유효한 프로파일을 코디네이터에 넣고 직렬화 실패를 던지는 작업을 실행한 결과와, 플랫폼 런타임 자동설정이 자기 빈에 넣는 기본 프로파일을 출력한 터미널 기록." caption="언제나 재시도 금지를 돌려주는 정책과 카운터 투입 · 정책 호출 0 · 작업 호출 2 · 플랫폼 기본 프로파일도 같은 자리로 — 30줄 · exit 0" zoom="true" :::

언제나 재시도 금지를 돌려주는 정책에 호출 카운터를 달았다. 프로파일은 두 번까지 허용하는 유효한 것이고, 작업은 직렬화 실패를 던진다.

정책 호출 0, 작업 호출 2 다. 정책은 재시도하지 말라고 말할 기회를 얻지 못했고, 작업은 호출자 프로파일에 따라 두 번 실행됐다. 예외도 경고도 없었다. 코디네이터가 하는 검증은 정책을 받았다는 널 아님 확인뿐이고, 그것이 쓰이지 않는다는 신호는 아니다.

플랫폼 자신도 같은 문에 걸린다

런타임 자동설정이 jpa-platform-default 라는 이름과 시도 3회짜리 프로파일로 정책을 만들어 넘긴다. 그 정책도 대체 정책 필드에 담긴 뒤 읽히지 않는다.

실행마다 쓰이는 것은 호출자가 준 프로파일에서 그 자리에서 만든 정책이다. 도달하지 못하는 것은 공개된 확장점만이 아니라 플랫폼이 자기 빈에 넣은 기본값이기도 하다.

고칠 방향은 셋이다

생성자가 받은 정책을 권위로 삼고 예산만 프로파일에서 계산하거나, 오버로드를 없애고 재시도 프로파일이 유일한 출처임을 API 로 못박거나, 정책과 프로파일을 하나의 객체로 합치는 것이다. 분석 문서는 지금 형태가 가장 나쁘다고 적는다. 설정 출처를 둘 받아 놓고 한쪽을 조용히 버리기 때문이다.

확인하지 못한 것

실제 데이터베이스를 붙인 통합 실행에서 같은 결과가 나오는지는 확인하지 않았다. 탐침은 트랜잭션 관리자를 대역으로 세운 단위 실행이다.