# 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`는 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`로 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` 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 ```