Files
clean-architecture-frontend…/docs/architecture/decisions/VD-23-api-transport-selection-and-rest-execution.md
T
DongHyeonkaandClaude Opus 5 e06e4377ca fix: preserve command effect certainty across retries
Separate per-attempt physical state from the logical execution history. The
executor now keeps one monotonic certainty accumulator joined through
joinMutationEffectCertainty, records MAYBE_APPLIED at dispatch, and reads the
accumulator from every retry-loop fence, final-invariant, cancellation and
timeout return.

A retry-time scope fence landing between the loop-entry check and the
pre-dispatch invariant can no longer downgrade an already dispatched command to
NOT_STARTED.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 23:01:48 +09:00

779 lines
28 KiB
Markdown

# VD-23: API transport selection과 REST execution
- 상태: Accepted — REST v2 security/execution baseline composed, advanced profiles pending
- 결정일: 2026-07-28
- installed REST reference vertical: `COMPOSED`
- REST v2 security/execution baseline: `COMPOSED`
- provider/path/auth profile baseline: `COMPOSED`
- conditional execution/provider-conformance delta: `DESIGNED_NOT_IMPLEMENTED`
- GraphQL/Connect/gRPC-Web/REST Gateway product selection: `NOT_SELECTED`
- 관련 결정: VD-13, VD-24, VD-25, VD-26, VD-27, VD-29, VD-30
- 상세 설계:
[API contract, Schema, Mapper와 Server State](../api-contract-schema-mapper-and-server-state.md)
- browser Protobuf/gateway 설계:
[Protobuf browser transport와 REST Gateway](../protobuf-browser-transport-and-rest-gateway.md)
## 배경
현재 reference feature는 operation registry, request Zod schema, shared HTTP
executor, response envelope/payload schema, mapper, gateway, application input과
TanStack Query까지 실제 production composition에 연결된다. 따라서 REST 경로
자체를 미구현으로 표시하지 않는다.
현재 reference operation은 v2 metadata, collision-aware composition, allowlisted
credential patch, auth fail-before-fetch, prefix-preserving target, shared logical
deadline, exact JSON media/success status, bounded decoder와 typed mapping failure를
실행한다. correlation header, success status group과 401 replay를 포함한 physical
attempt도 terminal observation에 연결한다.
operation은 path placeholder↔codec key exact join, named provider/bearer/CSRF
profile, provider credential-mode ceiling과 encoded query byte ceiling도 실행한다.
남은 production delta는 cookie-CSRF/CORS evidence, 204/304/412 conditional
transaction, artifact digest와 실제 provider conformance다.
`API_CONTRACT_VERSION` 문자열 일치만 backend compatibility 증거로 사용하지 않는다.
## 결정
### 1. Protocol 선택은 operation registry가 소유한다
feature application port는 protocol-neutral이다.
```text
feature use case
-> feature gateway
-> exact semantic operation
-> REST adapter
-> persisted GraphQL adapter
-> Connect adapter
-> gRPC-Web adapter
```
-`operationId`는 한 protocol에만 binding한다.
- UI, query hook과 use case는 protocol을 선택하지 않는다.
- URL, GraphQL document, service/method와 generated request를 application input으로
받지 않는다.
- 같은 command를 provider failure 때문에 다른 protocol로 자동 replay하지 않는다.
- read fallback도 동일 auth/freshness/schema/mapper/query identity와 하나의 total
retry budget을 증명한 registered policy만 허용한다.
- GraphQL/Connect/gRPC-Web adapter를 공통 `HttpClient`의 mode flag로 넣지 않는다.
공통으로 공유하는 것은 execution context, auth collaboration, failure vocabulary와
observation뿐이다.
### 2. REST operation은 실행 source와 정적 manifest를 분리하지 않는다
목표 API는 feature가 generic이 연결된 definition을 만든다.
```text
defineRestOperation<
Path,
Search,
Body,
SuccessWire,
SuccessValue,
Failure
>({
common,
providerId,
method,
uriTemplate,
pathCodec,
searchCodec,
bodyCodec,
successCodec,
errorCodec,
mapper,
policies
})
```
실행 definition에서 registry manifest를 결정적으로 투영한다. schema metadata,
실제 codec map, operation-specific TypeScript map과 mapper를 서로 다른 string
dispatch table에 수기로 중복하지 않는다.
manifest 최소 field:
```text
protocol = REST
registryVersion
providerId
operationId
owner
semantics
method
relativePathTemplate
pathSchemaId
searchSchemaId
bodySchemaId
requestMediaProfile
successStatusProfiles[]
errorStatusProfiles[]
responseSchemaId
mapperId
authProfileId
csrfProfileId
replayPolicy
idempotencyKeyPolicy
deadlineProfileId
retryProfileId
paginationProfileId | null
conditionalProfileId
serverStateProfileId | null
maxRequestBytes
maxDecodedResponseBytes
maxResponseItems
observabilityProfileId
compatibility
```
### 3. Contribution composer는 collision 전에 실패한다
```text
feature contributions[]
-> preserve every source row
-> validate contribution owner/version
-> detect duplicate operation/schema/mapper/query/topic ID
-> resolve every codec/mapper/profile reference
-> validate semantic coherence
-> freeze installed registry
-> emit compatibility manifest
```
object spread의 last-write-wins를 금지한다. duplicate ID가 payload까지 동일해도
owner를 하나 선택하지 않고 build를 실패시킨다. alias/rename은 versioned migration
row로만 허용한다.
semantic coherence:
- `GET`/`HEAD`는 body 없음, `SAFE`만 허용
- `HEAD`는 body success codec 없음
- `POST`/`PATCH` retry는 `IDEMPOTENT | KEYED_COMMAND` 또는 명시적
`SAFE` semantics 필요
- `KEYED_COMMAND``idempotencyKeyPolicy=REQUIRED`, 다른 replay policy의 key
attach/금지는 exact key profile과 일치
- `NON_REPLAYABLE`은 retry와 401 replay 금지
- success/error status가 겹치지 않음
- 204 profile은 body/schema 없음
- 304는 query + conditional profile + existing cache binding 필요
- 412는 precondition profile 필요
- path placeholder 집합과 path codec key 집합이 exact match
- query/cache profile은 `QUERY` operation에만 연결
- mutation invalidation은 registered topic만 사용
### 4. Provider endpoint와 URI
application은 provider URL을 받지 않는다.
```text
RestProviderProfile
providerId
baseOrigin
basePathPrefix
allowedCredentialsModes
corsProfile
referrerPolicy
redirectPolicy = ERROR
defaultHeaders
allowedResponseOrigins
```
- non-local은 HTTPS만 허용한다.
- provider는 credential mode ceiling만 제공하고 operation의 auth transport
profile이 exact mode를 선택한다. anonymous는 `omit`, same-origin cookie는
approved `same-origin`, approved cross-origin cookie만 `include`다.
- base URL의 username, password, query와 fragment를 금지한다.
- exact origin과 canonical base path prefix를 보존한다.
- operation template은 relative API path이며 scheme, authority, query와 fragment를
포함하지 않는다.
- leading slash가 base prefix를 제거하는 `new URL()` ambiguity를 쓰지 않는다.
- encoded slash, dot segment, NUL/control, duplicate slash와 overlong path를
정책대로 거절한다.
- redirect는 기본 `error`다. 로그인/다운로드 handoff는 일반 JSON REST operation과
다른 capability다.
path parameter:
- path codec의 parsed output만 encode한다.
- missing, extra, empty와 length 초과 parameter를 network 전에 거절한다.
- Unicode normalization을 업무 ID에 임의 적용하지 않는다.
- path value와 최종 URL은 diagnostics에 기록하지 않는다.
query:
- key ordering, repeated-array/comma style, boolean, null/absent/empty semantics를
profile에 고정한다.
- URL encoded byte ceiling을 적용한다.
- raw `URLSearchParams`, query string과 next URL을 caller에게 받지 않는다.
- sensitive/private value를 GET query에 넣는 operation은 별도 security review가
없으면 금지한다.
### 5. Request projection과 final invariant
request는 다음 소유 순서로 만든다.
```text
operation + validated input
-> immutable request binding
-> body canonical serialization + digest
-> transport-owned headers/options
-> constrained auth/CSRF patch
-> final invariant validation
-> fetch
```
transport-owned header:
- `Accept`, `Content-Type`
- contract/media version
- bounded correlation/trace context
- idempotency key
- conditional validator
- approved CSRF header
caller와 feature mapper가 arbitrary header를 추가하지 않는다.
auth owner target:
```text
CredentialPatch
credentialMode
allowlisted header name/value
proof expiry/generation
```
operation auth profile은 final Fetch `credentials`를 exact하게 고정한다.
`ANONYMOUS | BEARER_HEADER`는 ambient cookie가 섞이지 않게 `omit`,
same-origin cookie session은 `same-origin`, cross-origin cookie는 별도 CORS/CSRF
provider evidence가 있는 profile만 `include`다. provider ceiling과 맞지 않으면
fetch 0회다.
auth owner가 `Request` 전체를 반환하지 않는다. transition 기간에 current port를
사용한다면 attach 전후의 다음 값이 exact하게 같아야 한다.
- URL/origin/path/query
- method
- body digest
- content type/length
- correlation, idempotency, conditional와 CSRF binding
- redirect/cache/referrer/credentials/mode
다르면 `AUTH_INTEGRATION_FAILURE`, fetch 0회다.
auth-required operation은 session state가 `authenticated`가 아니면 fetch하지 않는다.
`integration-failed`, `unauthenticated`와 credential attach rejection을 anonymous
request로 downgrade하지 않는다.
#### 5-0. Logical effect certainty는 단조 증가한다
`PhysicalAttemptState`는 현재 attempt만 설명한다. logical execution 전체에는
별도의 monotonic accumulator를 두고 `joinMutationEffectCertainty`로 join한다.
join 순서는 보수적이다.
```text
NOT_STARTED < NOT_APPLIED < MAYBE_APPLIED < APPLIED_CONFIRMED
```
`fetch()` dispatch 시점에 command는 즉시 `MAYBE_APPLIED`를 기록한다. 이후 retry
loop entry, pre-dispatch final invariant, scope fence, cancellation, timeout
return은 모두 accumulator를 읽는다. 아직 보내지 않은 새 retry가 있다는 이유로
전체 logical operation을 `NOT_STARTED`로 되돌리지 않는다. query operation은
`NOT_APPLICABLE`로 남고 이 lattice를 쓰지 않는다.
#### 5-1. Installed auth profile registry (V3 집행)
`installRestAuthProfileRegistry()`가 composition 시점에 profile을 한 번 설치하고
`INSTALLED_REST_AUTH_PROFILES`가 유일한 authority다. contract composition
(`assertExecutionPolicy`)은 등록되지 않은 `authProfileId`를 거절하므로 executor는
runtime에 profile을 발명하지 않는다. profile은 다음을 exact하게 소유한다.
- Fetch `credentials` (credential collaborator가 바꿀 수 없다)
- `allowedCredentialHeaders`: 이 operation이 허용하는 정확한 proof header 집합
- `requiredCredentialHeaders`: dispatch 전에 반드시 관찰되어야 하는 집합
`CredentialPatchOutcome.READY`는 proof header만 담는다. `credentials` field는
제거되었다. credential owner가 transport-owned header(`accept`, `content-type`,
`idempotency-key`)나 forbidden header를 넣거나, profile이 허용하지 않는 header를
넣거나, required header를 빠뜨리면 `AUTH_INTEGRATION_FAILURE`이고 fetch 0회이며
command effect는 `NOT_STARTED`다. `idempotency-key`는 contract-owned이므로 더
구체적인 `UNEXPECTED_IDEMPOTENCY_KEY` request violation으로 남는다.
`UNAUTHENTICATED`는 user/session state이지 integration failure가 아니다.
transport-owned header는 credential header 뒤에 기록되어 key ordering으로도
shadow될 수 없고, final invariant가 `init.credentials`와 profile을 다시 대조하며
allowed/required credential header 집합을 독립적으로 재검증한다.
`AUTH_MODE=demo`는 profile을 약화시키지 않는다. `createDemoSessionAdapter`
고정된 비밀 아닌 `DEMO_AUTHORIZATION_MARKER` proof header를 제공하여 strict
`REFERENCE_EXTERNAL_BEARER`를 그대로 만족시킨다. 진짜 anonymous backend는 별도
anonymous contract/profile을 composition에서 선택해야 한다.
credential collaborator는 `AuthOperationContext { signal, deadlineAtMonotonicMs }`
받는다. cooperative owner는 스스로 중단하고, non-cooperative owner도 executor가
같은 lifetime signal과 race하므로 operation 수명을 넘기지 못하며 late completion은
관찰되지 않는다.
### 6. Cookie auth, CSRF와 CORS
same-origin BFF cookie session을 기본 권장한다.
- cookie는 Secure/HttpOnly이며 provider가 SameSite 정책을 소유한다.
- unsafe method는 server의 exact Origin/Fetch Metadata 검증과 composition-issued
anti-CSRF proof를 요구한다.
- CSRF proof는 application/query/cache에 노출하거나 persistence하지 않는다.
- custom content type/preflight가 있다는 사실만 CSRF 방어로 간주하지 않는다.
cross-origin profile은 다음을 actual provider에서 증명한다.
- exact `Access-Control-Allow-Origin`, wildcard 금지
- credentials mode와 allow-credentials 일치
- exact allow-method/allow-header
- 필요한 request ID, ETag, Retry-After만 expose
- OPTIONS와 actual response의 policy 동등성
- redirect 없음
### 7. Replay와 idempotency
```text
ReplayPolicy
SAFE
IDEMPOTENT
KEYED_COMMAND
NON_REPLAYABLE
```
- `SAFE`: read-only이며 network/recovery retry 가능
- `IDEMPOTENT`: 같은 principal/operation/payload/precondition의 반복 request가
의도한 server effect를 추가로 만들지 않으며 duplicate response/status mapping을
backend contract가 명시한다. response byte가 항상 동일하다는 뜻은 아니다.
- `KEYED_COMMAND`: application logical command lease가 key를 생성하고 lifecycle
전체에서 유지
- `NON_REPLAYABLE`: ambiguous result에서 자동 재실행 금지
`KEYED_COMMAND` binding:
```text
principal scope
operation ID/version
canonical payload digest
idempotency key
server retention/expiry
```
backend는 atomic claim, concurrent same-key join/replay, same-key different-payload
rejection과 terminal receipt를 제공한다. header를 보냈다는 사실만 replay safety가
아니다. provider conformance가 없으면 retry/401 recovery traffic을 켜지 않는다.
current client가 execute 호출마다 key를 생성하는 방식은 network attempt 안에서는
재사용되지만 ambiguous terminal 뒤 사용자 retry와 연결되지 않는다. 목표 command
owner가 effect certainty를 다음처럼 반환한다.
```text
NOT_APPLIED | COMMITTED | UNKNOWN
```
`UNKNOWN`은 새 key 자동 retry가 아니라 status/reconcile 또는 명시적 사용자 복구로
닫는다.
### 8. Logical deadline, cancel과 retry
```text
LogicalExecutionBudget
totalDeadlineMs
attemptTimeoutMs
maxAttempts
maxCumulativeSleepMs
maxRetryAfterMs
authRecoveryCount = 0 | 1
```
total deadline은 다음 모두를 포함한다.
- operation/schema lookup과 request encode
- credential/CSRF attachment
- fetch attempt
- body read/decode/schema/mapper
- 401 recovery
- retry backoff/Retry-After
각 phase는 남은 total budget보다 긴 timer를 만들지 않는다. timer, abort listener,
response reader와 auth waiter는 모든 terminal path에서 정리한다.
초회, network/status retry와 401 recovery replay를 포함한 **모든 API provider
fetch**는 하나의 monotonic `physicalAttemptCount`를 증가시키고 `maxAttempts`
소비한다. `authRecoveryCount`는 추가 상한일 뿐 attempt counter, sleep budget이나
total deadline을 reset하거나 우회하지 않는다.
retry status는 operation의 exact subset만 허용한다.
```text
network failure
408
429
502
503
504
```
- 429와 503의 `Retry-After`를 injected clock으로 parse한다.
- hard ceiling을 넘는 Retry-After는 sleep하지 않고 terminal로 닫는다.
- full jitter와 attempt/sleep/elapsed 세 상한을 모두 적용한다.
- schema, mapper, 4xx validation/authz/conflict와 redirect failure는 retry하지 않는다.
- caller cancel, navigation supersede, runtime teardown, attempt timeout과 logical
deadline을 다른 safe failure로 유지한다.
- 401 recovery는 auth-required이면서 replay-safe한 operation만 한 번 수행한다.
- 동시 401은 session owner의 single-flight recovery를 공유한다.
- physical attempt, logical retry와 auth replay를 따로 관측한다.
### 9. Closed execution
public executor는 promise rejection 대신 항상 closed result로 끝난다.
```text
RestExecutionResult<T>
SUCCESS
VALIDATION_REJECTED
AUTH_REQUIRED | AUTH_INTEGRATION_FAILURE
REQUEST_ABORTED
REQUEST_ATTEMPT_TIMEOUT | REQUEST_DEADLINE_EXCEEDED
NETWORK_UNREACHABLE
RATE_LIMITED | SERVER_FAILURE
HTTP_FAILURE
CONTENT_TYPE_MISMATCH | BODY_LIMIT_EXCEEDED | MALFORMED_BODY
SCHEMA_MISMATCH | MAPPING_CONTRACT_VIOLATION
PRECONDITION_FAILED | CONFLICT
CONTRACT_INCOMPATIBLE
```
attempt timer와 전체 logical deadline은 error registry, safe user copy와 telemetry
bucket에서도 별도 closed kind로 유지한다. 둘 다 raw URL/timing detail을
노출하지 않는다.
operation lookup, URL construction, `Headers`, body serialization, `Request`,
auth collaboration, fetch, read, parse, schema와 mapper를 모두 catch/normalize
경계 안에 둔다. thrown value/body/header/URL을 failure에 복사하지 않는다.
### 10. Response admission과 bounded decoder
body를 `Response.json()`으로 바로 읽지 않는다.
```text
response
-> final URL/status/header admission
-> exact media type parser
-> present/valid Content-Length advisory preflight
-> bounded stream reader
-> actual browser-visible decoded byte count
-> UTF-8/profile decoder
-> JSON structural ceiling
-> envelope/status codec
-> operation response codec
-> mapper
```
browser Fetch의 `Response.body`는 일반적으로 content decoding 뒤 stream이므로
client가 actual wire/encoded byte를 신뢰성 있게 세었다고 주장하지 않는다.
- BFF/proxy/CDN가 encoded transfer와 decompression ratio ceiling을 집행한다.
- browser client는 present/valid `Content-Length`를 advisory rejection에만 쓰고
actual decoded bytes를 hard cap으로 센다.
- 표준 `JSON.parse` profile은 decoded-byte cap이 pre-parse resource guard이고
depth/node/key/string/item cap은 materialization 뒤 admission guard다.
- 구조 cap을 parse 중 강제해야 하는 더 큰 profile은 bounded tokenizing JSON
parser를 별도로 선택하고 actual browser evidence를 가져야 한다.
cap 초과, truncation과 invalid UTF-8에서 reader를 cancel하고 cache에 쓰지 않는다.
media parser는 type/subtype/parameter를 exact하게 해석한다.
- `application/json`
- approved vendor `application/*+json`
- RFC Problem Details profile
- explicit 204 no-content
`includes("application/json")` 검사는 목표 계약이 아니다.
각 success status는 response codec을 가진다. 현재 envelope는
`REST_ENVELOPE_V1` profile로 유지할 수 있지만 모든 REST provider에 강제하지
않는다. success status와 error body가 모순되면 status/profile 계약 실패다.
### 11. Error projection
backend error는 먼저 status/media별 codec을 통과한다.
- raw message, stack, body와 arbitrary extensions를 버린다.
- code/category는 operation error profile의 allowlist로 mapping한다.
- unknown backend code는 closed generic failure다.
- validation issue는 최대 count, path/code byte/charset와 allowed path를 제한한다.
- request ID, trace ID와 correlation ID도 length/charset cap을 적용한다.
- `retryable` backend boolean을 client retry authority로 사용하지 않는다.
- 401/403/404 existence-hiding은 provider/product policy를 따른다.
- 409 business conflict와 412 representation precondition failure를 분리한다.
### 12. Cursor pagination
```text
CursorPageWire<T>
items
nextCursor | null
hasMore
snapshotToken | null
```
response codec은 item count, item size, cursor/snapshot byte와 total decoded ceiling을
검증한다. mapper는 immutable `CursorPage<ApplicationProjection>`을 만든다.
- cursor는 opaque이며 decode/로그/telemetry 금지
- arbitrary next URL을 따라가지 않음
- filters/sort/scope/snapshot과 cursor를 exact binding
- same cursor 반복, `hasMore=true`인데 cursor 없음, non-progress loop 거절
- maximum pages/items/cache bytes 이후 fetch 중지
- TanStack infinite query의 root key에는 semantic filter만 넣고 cursor는 bounded
`pageParam`으로 관리
- offset pagination은 stable small dataset이 증명된 별도 profile만 허용
current reference list array는 complete pagination 구현이 아니다.
### 13. Conditional read와 optimistic concurrency
operation별 owner:
```text
ConditionalProfile
NO_STORE
APP_ETAG
BROWSER_HTTP_CACHE
APPLICATION_REVISION
```
모든 operation은 한 profile을 가져야 한다.
- `NO_STORE`, `APP_ETAG`, `APPLICATION_REVISION`은 Fetch `cache=no-store`.
- `BROWSER_HTTP_CACHE`만 exact browser cache mode와 server
`Cache-Control`/`Vary` contract를 사용한다.
- caller/library default에 맡기는 implicit `NONE`은 없다.
`APP_ETAG`:
- strong ETag/generation은 adapter-private metadata
- exact operation/query/scope/representation binding과 함께 memory에 보존
- `If-None-Match`를 transport가 생성
- 304는 same binding의 mapped cached value가 있을 때만 freshness 갱신
- 304는 same query-entry cache revision CAS가 성공할 때만 commit
- cached value가 없으면 one-time unconditional request 또는 closed failure
- app-managed conditional operation은 fetch cache mode를 `no-store`로 고정
- full 200 mapped commit과 validator install/update는 같은 entry transaction
- query removal/GC, scope/logout, release/contract/schema/mapper epoch reset에서
validator sidecar도 함께 폐기
- ordinary invalidation에서 validator 보존 여부는 profile이 고정
`BROWSER_HTTP_CACHE`:
- standard browser cache가 revalidation을 소유
- application이 hidden validator/304 logic을 중복 구현하지 않음
- HTTP `Cache-Control`을 TanStack `staleTime`으로 자동 변환하지 않음
write precondition:
- exact resource revision을 `If-Match` 또는 body contract로 binding
- 412는 `PRECONDITION_FAILED`
- current server representation을 refetch한 뒤 feature use case가 overwrite,
merge 또는 cancel을 결정
- 409 business conflict와 합치지 않음
ETag/revision은 authorization proof가 아니며 raw value를 diagnostics에 넣지 않는다.
### 14. Schema와 Mapper 연결
VD-24의 typed codec/mapper를 사용한다.
```text
unknown
-> RuntimeCodec<ValidatedResponseDto>
-> Mapper<ValidatedResponseDto, ApplicationProjection>
-> Result
```
request는 strict/normalized parsed output만 serialize한다. ordinary additive
response는 required/discriminant를 검증하고 unknown field를 폐기한다. control,
authorization와 sealed union은 unknown을 거절한다.
generated OpenAPI client/DTO를 선택해도 adapter-private이다. handwritten gateway와
mapper를 제거하지 않는다.
### 15. Query cache와의 관계
REST transport는 raw response cache를 소유하지 않는다. VD-25가 mapped application
projection을 TanStack Query에 admission한다.
- operation registry의 cache profile만 query를 만들 수 있다.
- Query retry는 `false`; REST transport가 network retry를 소유한다.
- URL, ETag, idempotency key, Response와 DTO를 query key/value에 넣지 않는다.
- mutation success 뒤 registered invalidation topic 또는 exact typed seed policy만
사용한다.
- REST timeout/error를 Query가 다시 network retry하지 않는다.
### 16. Contract source와 release coherence
OpenAPI가 backend authority인 제품:
```text
authenticated immutable OpenAPI artifact
-> source digest/provenance
-> lint + breaking diff
-> pinned deterministic generation
-> runtime codec parity
-> adapter-private DTO/client
-> handwritten mapper/gateway
```
CI:
- generator/runtime/plugin/Node version pin
- clean checkout regenerate diff 0
- stable operation ID
- source/artifact/generated output digest
- runtime codec vs specification fixtures
- N/N-1 and future-major failure
- generated import boundary
global `API_CONTRACT_VERSION`은 selected contract-set compatibility와 digest에
연결한다. runtime config와 release manifest의 같은 문자열만으로 backend
compatibility를 주장하지 않는다.
major version 전략은 URI version 또는 vendor media version 중 provider가 하나를
선택한다. 동시에 둘을 임의 증가시키지 않는다. v1/v2 adapters는 같은
application gateway를 구현할 수 있지만 같은 query entry에 representation을
섞지 않는다.
### 17. Security와 observability
관측 허용:
- semantic operation/provider/profile ID
- method/semantics
- outcome/AppFailure kind/HTTP status group
- logical retry, auth recovery와 physical attempt bucket
- duration/deadline/request-response byte/item bucket
- conditional/cache outcome
금지:
- URL, path/search/header/body
- cursor/snapshot/ETag/revision
- idempotency/CSRF/auth token
- raw backend code/message/request/trace value
- application resource/account/tenant ID
client correlation ID는 bounded syntax로 request에 전달하고 server-projected
request/trace ID는 safe failure/observation에만 제한한다. 한 logical execution은
terminal event 하나를 만든다.
### 18. Composition과 readiness
REST v2 composition owner가 다음을 atomic하게 만든다.
```text
parse provider/config
-> compose collision-free operation registry
-> resolve codec/mapper/policy references
-> install auth/CSRF owner
-> run compatibility/provider probes
-> publish READY facade
```
partial registry/client를 application에 노출하지 않는다.
```text
Selection
TrafficAdmission
RuntimeHealth
PromotionEvidence
```
primary status가 `COMPOSED`여도 provider/auth/CSRF/idempotency/conditional
conformance가 없으면 `TrafficAdmission=DISABLED`
`PromotionEvidence=MISSING | PARTIAL | EXPIRED`로 닫는다.
kill switch:
- provider 전체
- operation family
- keyed retry/401 replay
- conditional request
- optimistic mutation
- query cache admission
### 19. Test와 provider conformance
deterministic:
- contribution collision, missing codec/mapper/profile와 owner mismatch
- URI template/path/query canonicalization/base-prefix preservation
- method/body/replay/status/media coherence
- anonymous/cookie/bearer credentials와 Fetch cache mode matrix
- auth final-request mutation 공격과 unavailable auth fetch 0
- attempt timeout vs total deadline, timer/listener/reader cleanup
- concurrent 401 single-flight
- 401 replay를 포함한 monotonic physical-attempt cap
- retry matrix, injected-clock 429/503 Retry-After
- same key/same payload replay와 same key/different payload rejection
- 204/304/412/422/problem/envelope
- oversized/truncated/malformed/decompression overflow
- cursor loop/snapshot/page ceiling
- ETag 304 without cache, If-Match 412
- mapper failure와 redaction
actual staging provider:
- HTTPS/base path/CORS/preflight/credential
- cookie/Origin/CSRF
- idempotency concurrent claim/TTL/reconcile
- status/media/error codec
- cursor/snapshot/conditional semantics
- rate limit/Retry-After
- correlation/request/trace projection
- proxy/CDN content encoding and body cap
- browser decoded-byte cap과 provider encoded/decompression ceiling
- outbound correlation, success status group와 401 physical-attempt observation
MSW 통과는 provider conformance가 아니다.
### 20. Rollout
1. collision-aware v2 registry/codec과 boot-time binding 검증을 설치한다.
2. auth fail-closed, final invariant, bounded decoder와 total deadline을 local
reference vertical에서 검증한다.
3. actual provider에 같은 fixture를 실행하고 read operation을 canary한다.
4. keyed command는 backend idempotency conformance 뒤 별도 canary한다.
5. pagination/conditional operation을 각각 별도 traffic gate로 올린다.
6. provider/browser/operations evidence가 complete인 operation만 enabled한다.
7. rollback은 우선 safe unavailable로 내리고 contract artifact/frontend/backend를
coherent set으로 복구한다. v1 fallback은 해당 operation의 unexpired
provider/security evidence가 있고 incident가 v1/shared boundary에 영향이 없으며
auth fail-close/final invariant hardening이 유지될 때만 허용한다.
### 21. Removal
GraphQL/Connect/gRPC-Web/REST Gateway 선택을 취소해도 REST v2 common execution
context는 남을 수 있다.
REST provider 제거 시:
1. 신규 operation admission 중지
2. read cancel, command effect certainty reconcile
3. auth/CSRF/retry timer와 response reader close
4. current scope query cache clear/invalidate
5. operation/schema/mapper/query profile 제거
6. provider config/proxy/dependency/fixture 제거
7. production module inventory와 backend route retirement evidence
## 완료 기준
- installed REST operation이 typed path/search/body/success/error codec과 mapper에
하나의 definition으로 연결된다.
- auth unavailable 또는 mutated final request에서 fetch가 0회다.
- 모든 throw/response size/status/media/schema/mapper failure가 closed result다.
- total logical deadline이 auth/recovery/backoff/decode/mapper를 포함한다.
- replay는 declared policy와 actual backend idempotency evidence를 가진다.
- complete cursor page와 conditional/412 state가 bounded하게 동작한다.
- response DTO/URL/header/token/validator가 application/query/log에 없다.
- actual provider conformance와 rollback/removal drill이 통과한다.