Files
document-haness/docs/clean-architecture-backend-template/analysis/02-shared-contract.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

234 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# shared-contract 상세 분석
## SSOT identity — 2026-08-31 재검증
- registered leaf id: `shared-contract`
- canonical state `analysisFile`: `analysis/02-shared-contract.md` (이 문서) — 이 leaf의 단일 SSOT
- source path: `src/shared-contract` · Gradle `:shared-contract`
- registry `allowed_dependencies`: **`[]`**
- registry `runtime_memberships`: `["app-bootstrap", "sample-portfolio"]`
- coverage ledger: `FULL_READ` **82** / `STRUCTURAL_ONLY` **4** / `EXCLUDED` **0** / `UNCLASSIFIED` **0**
- 최초 분석 revision `a24ece9c` → 재검증 revision `21234e38` · 이 리프의 변경 파일 **0**
- 재검증 증거: `EVD-333`(소스 드리프트 0), `EVD-334`(lane 재실행)
> 재검증이 확인한 것은 대상이 움직이지 않았다는 사실이지, 아래 서술이 옳다는 보증이 아니다.
> 이번 사이클에서 코드에 대고 다시 확인한 항목은 이 문서의 검증 절과 위 증거가 가리키는 범위다.
---
## 분석 상태
- scope: `shared-contract`
- source path: `src/shared-contract`
- Gradle path: `:shared-contract`
- source revision: `a24ece9cf797f7ea647e33bf846b115208ed1ba5`
- analysis cycle: 1 / normal
- result: COMPLETE
- registry dependencies: production project dependency 0
- runtime memberships: `app-bootstrap`, `sample-portfolio`
## 역할과 경계
`shared-contract`는 특정 도메인이나 Spring/Jackson/JPA 구현을 소유하지 않고 여러 adapter와 composition root가 공유하는 운영 계약을 보관하는 leaf module이다. `build.gradle`의 production dependency block은 비어 있으며, `CLAUDE.md`도 Java standard library only를 명시한다. 실제 production source에서도 Spring/Jackson/JPA type은 관찰되지 않았다.
이 모듈이 제공하는 계약은 단일 관심사라기보다 다음의 skeleton-wide boundary 묶음이다.
- error taxonomy와 framework-neutral exception carrier
- API response/bulk/pagination/long-running-operation shape
- partial-update의 3-state `Patch`
- `resource:action` permission value
- provider-neutral edge rate-limit contract
- metric naming/cardinality guardrail
- traceparent/baggage/span-error seam
- domain/business context propagation seam
- compare-and-set operational record storage port
- adapter master-switch parser
- Redis semantic health snapshot projection
- messaging envelope JSON Schema v1와 checked-in SHA-256 digest
따라서 이 module의 핵심 아키텍처적 의미는 "공통 유틸리티"가 아니라, 서로 다른 outer module이 한쪽 adapter의 type에 의존하지 않고 합의할 수 있는 중립 계약 지점이다. `OperationalRecordStorePort`의 실제 consumer인 GraphQL persisted-operation registry가 inbound adapter 자체의 저장소 interface를 선언하지 않고 이 중립 port에 의존하는 것이 그 방향성을 직접 보여준다.
## 주요 계약과 불변식
### Error contract
`ApiErrorCode`는 code/category/httpStatus/retryable의 최소 표면을 제공하고 `OperationalError`가 registry mirror 역할을 한다. `Category`는 VALIDATION, AUTH, AUTHZ, NOT_FOUND, CONFLICT, RATE_LIMIT, TRANSIENT_DEPENDENCY, PERMANENT_DEPENDENCY, DATA_INTEGRITY, INTERNAL의 10개 값으로 고정되어 있으며 테스트가 정확한 vocabulary를 pin 한다.
`OperationalErrorTest`는 단순 enum 존재보다 category × retryable 의미를 강하게 검증한다. deterministic VALIDATION/AUTHZ/NOT_FOUND는 retryable=false이고, transient INTERNAL은 기본적으로 retryable=true이되 deploy-time/configuration/terminal 상태인 일부 code는 명시적 예외로 false다. `AUTH_KID_UNKNOWN`은 key rotation 중 JWKS refresh 가능성을 이유로 AUTH 중 유일한 retryable case로 pin 되어 있다. upstream 4xx 전체를 permanent/non-retryable로 분류하면서 408/429의 의미 차이가 남는다는 점은 source comment와 README가 이미 known edge로 기록한다.
`DependencyFailureException``PersistenceFailureException``ApiErrorCarrier`를 통해 transport adapter에 stable error code를 전달하면서 raw cause/diagnostic message를 server-side 정보로 남긴다. `AdapterDisabledException`은 carrier를 구현하지 않고 별도 mapping 대상이다.
### Response / operation contract
`Envelope`, `BulkEnvelope`, `ResponseMeta`, `PageMeta`, `Operation`은 framework-neutral record/factory로 API shape를 전달한다. 여기서는 중요한 enforcement boundary 차이가 관찰된다.
`Envelope.ok/failure`, `BulkEnvelope.allOk/partial`, `Operation.pending/succeeded/failed` factory는 문서의 정상 shape를 생성하고 테스트도 이 factory path를 검증한다. 그러나 canonical record constructor 자체는 success/data/error의 배타성, operation status와 result/error의 조합, pagination 범위 등을 검증하지 않는다. 따라서 이 규칙은 rate-limit value object처럼 intrinsic constructor invariant가 아니라 factory/adapter usage contract다. 현재 source와 test가 일치하므로 즉시 결함으로 분류하지 않지만, raw constructor가 외부 module에 public인 만큼 invalid shape 생성 가능성은 P1 hardening 후보로 남는다.
`Patch<T>`는 ABSENT / PRESENT_NULL / PRESENT_VALUE의 3-state를 명확하게 보존하며 absent에서 `value()`를 호출하면 실패한다. 이는 JSON Merge Patch 계열에서 "필드 미전송"과 "명시적 null"을 구분해야 하는 boundary를 framework type 없이 표현한다.
### Permission
`Permission`은 정확히 한 개의 colon으로 `resource:action`을 분리하고 trim/lowercase normalization을 수행한다. 테스트는 mixed case, surrounding whitespace, blank component, 0/2+ colon을 검증한다. 다만 source는 component 내부 character set을 제한하지 않는다. 즉 "lowercase colon-delimited"는 normalization 결과이지 `[a-z0-9-]+` 같은 strict grammar는 아니다. 현재 test 역시 이를 요구하지 않으므로 observed contract로만 기록한다.
### Edge rate-limit contract
이 영역은 shared-contract 안에서도 가장 강하게 self-validating 된다. `RateLimitPolicy`, `RateParameters`, `RateLimitRequest`, `RateLimitDecision`, `RateLimitOutcome`, `RateLimitEvaluationDedupPolicy`가 생성 시점에 bounded representation과 arithmetic safety를 검증한다.
- Lua exact integer range를 `9_007_199_254_740_991`로 제한한다.
- sliding counter/token bucket fixed-point 계산에 scale `1_000_000`을 사용하며 중간 합/곱도 exact-range를 넘지 않게 검증한다.
- window/cleanup/retry duration은 whole milliseconds만 허용하고 상한을 둔다.
- policy id/revision, subject digest, evaluation id는 bounded regex로 제한한다.
- raw edge identity는 `EdgeRateLimitSubject`에서만 잠시 존재하고 provider request에는 pseudonymous digest만 전달하도록 type/regex로 가드한다.
- v1 failure policy는 `FAIL_CLOSED`만 허용한다.
- unavailable / indeterminate / incompatible를 evaluated denial과 분리하여 transport/provider ambiguity를 숨기지 않는다.
- response-loss replay dedup은 TTL, entry count, logical stored bytes를 동시에 제한한다.
별도 `edgeRateLimitContractTest` source set이 provider-neutrality와 bounded request semantics를 qualification lane으로 다시 pin 한다.
### Metrics and tracing
`MetricNaming`은 Micrometer-facing dot.case naming과 seconds/bytes/total suffix vocabulary를 framework dependency 없이 보존한다. `CardinalityBounds`는 bounded tag의 상한을 Java mirror로 제공하고, `ForbiddenMetricTags`는 request_id/user_id/raw URL/query/header/IP 같은 unbounded source를 metric label에서 금지한다. `request_id`가 baggage에는 허용되지만 metric label에는 금지되는 비대칭은 test에서 의도적으로 pin 되어 있다.
`TraceParent`는 이 skeleton이 지원하는 strict v00 subset을 parse/render한다. lowercase hex, non-zero trace/span id, 2-byte flags를 검사하고 wrong version을 거부한다. `BaggageAllowlist``tenant_id`, `request_id`만 보존하는 단순 parse/filter/render utility다. 이는 full W3C baggage grammar validator라기보다 propagation boundary allowlist다. `SpanErrorRecorder.NOOP`은 tracer library가 없는 기본 template에서도 outer adapter가 동일 seam을 호출할 수 있게 한다.
### Domain context propagation
`DomainContextPropagator`는 diagnostic MDC와 분리된 domain/business context channel이다. default `ThreadLocalDomainContextPropagator`는 plain `ThreadLocal`을 쓰고 implicit inheritance를 금지하며 `capture()/restore()``wrap()`으로 명시적 hand-off를 수행한다. virtual-thread test는 wrap을 썼을 때 전달되고 쓰지 않았을 때 상속되지 않으며 scope close 뒤 worker context가 복원되는 것을 검증한다.
이 seam은 문서상 계획에 그치지 않는다. production reachability 검색에서 `app-bootstrap``DomainContextConfig`, `AsyncContextTaskDecorator`, `AsyncExecutorConfig`, persistence-jpa audit adapter, sample composition config가 실제로 소비하는 것이 확인됐다.
`DomainContextKey` equality/hash는 **name only**이고 read 시 요청 key의 `Class<T>`로 cast한다. 동일 이름의 서로 다른 type key를 만들면 같은 slot을 공유할 수 있고 잘못된 type으로 읽을 때 `ClassCastException` 가능성이 있다. source javadoc이 name-only identity를 명시하므로 hidden implementation bug로 단정하지 않지만, 현재 test는 same-name/different-type collision을 pin 하지 않는다. P1 contract-hardening 후보로 남긴다.
### Operational record store
`OperationalRecordStorePort`는 durable operational state를 특정 inbound/outbound adapter에 종속시키지 않는 neutral CAS port다. record version 0은 absent를 뜻하며 compareAndSet/compareAndRemove의 expectedVersion이 lost update 방지 evidence 역할을 한다. GraphQL persisted-operation adapter가 이 port를 실제 production dependency로 사용하며, durable provider implementation 자체는 해당 inbound adapter에 들어있지 않다.
`OperationalRecord`는 namespace/key non-blank와 version >= 0은 강제하지만 javadoc의 "bounded"라는 표현에 대응하는 길이/character limit은 source에 없다. 이는 문서와 constructor enforcement 강도의 차이이며 P2 확인 후보로 남긴다.
### Activation and health snapshot
`MasterSwitchParser`는 unset=false, true/false case-insensitive만 허용하며 `yes`, `1`, `on`, whitespace-padded value를 invalid로 처리한다. canonical+legacy가 동시에 있으면 값이 같아도 ambiguous로 실패하고 legacy-only는 replacement property를 반환한다. 이는 operator configuration을 permissive coercion하지 않는 fail-closed contract다.
`RedisHealthSnapshotProvider`는 Redis client/connection/credential을 shared boundary로 새지 않게 role/capability/state/reason/semantic freshness만 projection한다. eviction policy는 runtime CONFIG 조회 증명이 아니라 configured expectation임을 enum 이름과 javadoc으로 명시한다.
### Messaging envelope schema
`contracts/messaging/envelope/v1.schema.json`은 Draft 2020-12 schema resource이며 envelopeVersion/eventId/contractId/payloadVersion/logicalDestination/aggregate/occurredAt/correlationId/contentType/payload를 required로 고정하고 top-level/aggregate에 `unevaluatedProperties:false`를 둔다. checked-in SHA-256은 `bf6f2e13fafe01b8ef4cbb73d7ba3f5703bfc68d145bdfe43190bf606dbd00b1`이다.
`MessagingEnvelopeSchemaResourceTest`는 schema text 자체, identifier regex parity, Java int/long 경계 vector, strict UTF-8, exact digest를 JDK API로 검증한다. 이 테스트는 resource drift와 digest mismatch를 강하게 막지만 README가 명시하듯 실제 Draft 2020-12 validator interoperability나 broker runtime discovery를 증명하지는 않는다.
## Reachability / wiring evidence
- `DomainContextPropagator`: app-bootstrap composition + async decorator, persistence-jpa audit adapter, sample composition에서 production use 확인.
- `OperationalRecordStorePort`: inbound GraphQL persisted-operation registry에서 production use 확인. 이 방향성은 adapter-specific storage interface를 outbound가 구현하는 역방향 dependency를 피한다.
- registry상 shared-contract는 다른 production project를 의존하지 않는 leaf이며 app-bootstrap/sample-portfolio runtime membership을 가진다.
- rate-limit, response, error 등의 세부 consumer 전체는 각 adapter/application bounded scope에서 추가 분석할 대상이며 이번 scope에서는 representative reachability와 contract 자체를 완전 읽기 대상으로 삼았다.
## Verification
실제 실행 결과:
- `./gradlew :shared-contract:test --console=plain` → BUILD SUCCESSFUL, exit 0
- `./gradlew :shared-contract:edgeRateLimitContractTest --console=plain` → BUILD SUCCESSFUL, exit 0
- source revision 확인: `a24ece9cf797f7ea647e33bf846b115208ed1ba5`
- `git status --short` → output 없음, working tree clean
## Coverage ledger
분모는 `src/main` 전체 파일, `src/test` 전체 파일, custom `edgeRateLimitContractTest` source, 그리고 module-level `CLAUDE.md`, `README.md`, `build.gradle`이다.
- FULL_READ: 82
- main production/resource 55
- unit/contract test 23
- edgeRateLimitContractTest 1
- module policy/rationale/build 3
- STRUCTURAL_ONLY: 4
- production placeholder `.gitkeep` 3
- test placeholder `.gitkeep` 1
- EXCLUDED: 0
- UNCLASSIFIED: 0
따라서 selected bounded scope는 completion standard를 충족한다.
## Open questions / improvement backlog
### P1 — response/LRO invariant enforcement boundary
`Envelope`, `BulkEnvelope`, `Operation`, `PageMeta`의 문서상 valid shape가 factory tests에는 고정되어 있지만 public canonical constructor에서 강제되지 않는다. raw constructor 사용이 실제로 허용된 extension surface인지, 아니면 constructor-level validation으로 invalid state를 막아야 하는지 결정이 필요하다.
### P1 — DomainContextKey same-name different-type collision
key identity가 name only인 반면 retrieval은 requested type cast를 수행한다. 동일 name의 다른 `Class<T>` key를 선언하는 것이 forbidden contract라면 creation-time collision 방지 또는 registry rule/test가 필요하고, 의도적으로 허용한다면 failure semantics를 문서화할 필요가 있다.
### P2 — bounded operational record identifiers
`OperationalRecord` javadoc은 namespace/key를 bounded라고 설명하지만 constructor는 blank 여부만 확인한다. provider key size/character-set 제한을 shared contract가 소유해야 하는지 확인이 필요하다.
### P2 — permission component grammar
permission은 colon segment 수, blank, normalization은 강제하지만 segment character grammar는 제한하지 않는다. registry SSOT가 더 좁은 grammar를 요구한다면 shared value object와 parity test가 필요하다.
### P2 — messaging schema qualification boundary
현재 JDK-only test는 exact resource/digest/selected semantic vectors를 검증한다. Draft 2020-12 validator 호환성은 별도 qualification evidence가 필요하며 현재 module test 성공만으로 이를 주장해서는 안 된다.
## 다음 scope
queue의 동일 active project를 유지하고 다음 PENDING scope인 `application-core`를 다음 실행에서 분석한다.
## Source anchors
이 문서가 backtick으로 인용한 타입·경로를 저장소 트리에 대고 해석한 결과다. 해석된 것만 싣는다 — 총 **40개** (main 35 · test 2 · 기타 3).
```
src/shared-contract/build.gradle
src/config/architecture/modules.json (shared-contract 항목)
main:
src/main/java/dev/caskeleton/shared/activation/MasterSwitchParser.java
src/main/java/dev/caskeleton/shared/concurrency/DomainContextKey.java
src/main/java/dev/caskeleton/shared/concurrency/DomainContextPropagator.java
src/main/java/dev/caskeleton/shared/concurrency/ThreadLocalDomainContextPropagator.java
src/main/java/dev/caskeleton/shared/error/AdapterDisabledException.java
src/main/java/dev/caskeleton/shared/error/ApiErrorCarrier.java
src/main/java/dev/caskeleton/shared/error/ApiErrorCode.java
src/main/java/dev/caskeleton/shared/error/Category.java
src/main/java/dev/caskeleton/shared/error/DependencyFailureException.java
src/main/java/dev/caskeleton/shared/error/OperationalError.java
src/main/java/dev/caskeleton/shared/error/PersistenceFailureException.java
src/main/java/dev/caskeleton/shared/health/RedisHealthSnapshotProvider.java
src/main/java/dev/caskeleton/shared/metrics/CardinalityBounds.java
src/main/java/dev/caskeleton/shared/metrics/ForbiddenMetricTags.java
src/main/java/dev/caskeleton/shared/metrics/MetricNaming.java
src/main/java/dev/caskeleton/shared/operation/Operation.java
src/main/java/dev/caskeleton/shared/opstore/OperationalRecord.java
src/main/java/dev/caskeleton/shared/opstore/OperationalRecordStorePort.java
src/main/java/dev/caskeleton/shared/ratelimit/EdgeRateLimitSubject.java
src/main/java/dev/caskeleton/shared/ratelimit/RateLimitDecision.java
src/main/java/dev/caskeleton/shared/ratelimit/RateLimitEvaluationDedupPolicy.java
src/main/java/dev/caskeleton/shared/ratelimit/RateLimitOutcome.java
src/main/java/dev/caskeleton/shared/ratelimit/RateLimitPolicy.java
src/main/java/dev/caskeleton/shared/ratelimit/RateLimitRequest.java
src/main/java/dev/caskeleton/shared/ratelimit/RateParameters.java
src/main/java/dev/caskeleton/shared/request/Patch.java
src/main/java/dev/caskeleton/shared/response/BulkEnvelope.java
src/main/java/dev/caskeleton/shared/response/Envelope.java
src/main/java/dev/caskeleton/shared/response/PageMeta.java
src/main/java/dev/caskeleton/shared/response/ResponseMeta.java
src/main/java/dev/caskeleton/shared/security/Permission.java
src/main/java/dev/caskeleton/shared/tracing/BaggageAllowlist.java
src/main/java/dev/caskeleton/shared/tracing/SpanErrorRecorder.java
src/main/java/dev/caskeleton/shared/tracing/TraceParent.java
src/main/resources/contracts/messaging/envelope/v1.schema.json
test:
src/test/java/dev/caskeleton/shared/contract/messaging/MessagingEnvelopeSchemaResourceTest.java
src/test/java/dev/caskeleton/shared/error/OperationalErrorTest.java
기타:
CLAUDE.md
README.md
src/build.gradle
```