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.4 KiB
kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, decisionStatus, decidedOn, source
| kind | slug | title | topic | project | status | sourceRevision | rootTreeNode | decisionStatus | decidedOn | source | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| PROJECT_DECISION | retry-unit-is-the-use-case | 재시도 단위는 statement가 아니라 유스케이스 전체다 | commit-ambiguity-as-a-result | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | decision:retry-unit-is-the-use-case | ADOPTED | 2026-08-11 |
|
재시도 단위는 statement가 아니라 유스케이스 전체다
결정문
재시도는 실패한 statement 나 트랜잭션이 아니라 유스케이스 전체를 새 트랜잭션과 새 Persistence Context 에서 다시 실행한다.
판단 이유
statement 수준 재시도는 지금 재시도하려는 실패들에 대해 정확히 틀린 선택이다. 낙관적 충돌은 시도가 계산의 기준으로 삼은 상태가 더 이상 커밋된 상태가 아니라는 뜻이므로, 같은 statement 를 다시 쏘면 이미 움직인 버전에 대해 같은 틀린 답을 계산한다. 도메인 규칙이 다시 읽은 데이터 위에서 다시 돌아야 하고, 그것은 곧 유스케이스 전체다.
Persistence Context 재사용도 같은 이유로 틀리다. 두 번째 시도가 1차 캐시에서 첫 시도의 낡은 엔티티를 읽게 된다.
advice 순서도 결정의 일부다. 재시도 advice 가 Spring 의 트랜잭션 advice 바깥에 놓여야 각 시도가 새 트랜잭션을 시작한다. 순서가 뒤집히면 재시도 루프가 이미 rollback-only 로 표시된 하나의 트랜잭션 안에서 돌고, 두 번째 시도는 아무것도 실행하지 못한 채 즉시 실패한다.
예산은 시도 횟수와 경과 시간 두 상한을 함께 갖는다. 코디네이터가 매 시도마다 경과를 계산해 두 상한을 함께 확인한다.
두 가지 실패는 예산과 무관하게 재시도하지 않는다. 완료를 알 수 없는 실패는 조정으로 가고, 정책이 실패로 분류한 것은 그대로 실패다.
영향
감수하는 것
재시도 가능한 유스케이스는 처음부터 다시 실행해도 안전해야 한다. 커밋 전에 되돌릴 수 없는 외부 효과가 있으면 안 된다.
그 조건이 성립하지 않는 유스케이스는 스스로 선언해야 한다. IrreversibleSideEffectContext 가 그 선언을 받고, 정책은 남은 예산과 무관하게 재시도를 거부한다.
재시도 한 번의 비용이 statement 재시도보다 크다. 유스케이스 전체가 다시 돈다.
얻는 것
낙관적 충돌과 직렬화 실패가 실제로 해소된다. 다시 읽은 데이터 위에서 도메인 규칙이 다시 판단하기 때문이다.
두 프로파일이 하나의 재시도를 나눠 결정하는 상황이 없다. 호출자가 넘긴 프로파일 하나가 적격성과 백오프와 예산을 모두 정한다.
2026-09-01 재검증 재검증 결과다.
결정은 채택되어 있고 코디네이터는 조건부 빈으로 생성된다. 그러나 그 빈을 주입받아 호출하는 프로덕션 코드가 없다.
ADR 의 Enforcement 절은 두 가지를 지목한다. 첫 번째인 FullTransactionRetryCoordinatorTest 는 실재하며 코디네이터 자체를 검증한다. 두 번째인 RetryableJpaTransactionInterceptor 는 이 저장소에 존재하지 않고 구현 계획과 코드 리뷰와 이 ADR 세 문서에만 이름으로 남아 있다.
app-bootstrap 과 persistence-jpa 의 main 에는 Advisor 도 MethodInterceptor 도 Pointcut 도 Aspect 도 없다.
따라서 현재 상태는 이렇다. 코디네이터의 동작은 테스트로 고정되어 있고, 그것이 어떤 유스케이스에 적용되는지는 고정되어 있지 않다. 이 절의 근거는 EVD-336 이다.
근거
- 번역되지 않은 경합 예외가 재시도 코디네이터의 catch를 통째로 비껴갔다 이 재시도 경로가 실제로는 돌지 않았던 사례다.
- 모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다 예산과 무관하게 재시도하지 않는 두 실패 중 하나가 이 규칙에서 나온다.