Files
tech-log-frontend/docs/architecture/decisions/VD-30-protobuf-contract-and-rest-gateway.md
T

1094 lines
42 KiB
Markdown

# VD-30: Protobuf contract와 REST transcoding gateway
- 상태: Accepted design — schema/codegen과 provider runtime implementation pending
- 결정일: 2026-07-28
- Protobuf schema/codegen reference governance:
`DESIGNED_NOT_IMPLEMENTED`
- provider-neutral REST transcoding reference:
`DESIGNED_NOT_IMPLEMENTED`
- product REST Gateway composition: `NOT_SELECTED`
- 현재 installed REST reference의 기본 public adapter: curated BFF
- direct REST Gateway default RPC kind: unary only
- REST server-stream/client-stream/bidirectional-stream composition:
`NOT_SELECTED`
- 관련 결정: VD-12, VD-13, VD-14, VD-23, VD-24, VD-25, VD-27, VD-29
- 상세 설계:
[Protobuf browser transport와 REST Gateway](../protobuf-browser-transport-and-rest-gateway.md)
- 상위 API 설계:
[API contract, Schema, Mapper와 Server State](../api-contract-schema-mapper-and-server-state.md)
- backend handoff:
[Backend API와 Server State contract](../backend-api-and-server-state-contract.md)
## 배경
현재 repository에는 authenticated `.proto` source, Buf module, `buf.yaml`,
`buf.lock`, `buf.gen.yaml`, immutable descriptor set, generated gateway,
generated OpenAPI, protobuf runtime dependency와 actual REST transcoding provider
evidence가 없다. 따라서 이 ADR은 production contract와 구현 순서를 정의하지만
capability 구현 완료 증거는 아니다.
VD-27의 gRPC-Web과 이 ADR의 REST Gateway는 같은 protobuf source를 사용할 수
있어도 같은 browser protocol이 아니다.
```text
gRPC-Web
browser generated protobuf client
-> gRPC-Web binary/text frame
-> gRPC-Web proxy
REST transcoding
browser ordinary HTTP/JSON client
-> google.api.http route + ProtoJSON
-> REST/gRPC transcoding gateway
```
gRPC-Web의 frame, terminal trailer와 client runtime을 REST Gateway에 재사용하지
않는다. REST Gateway의 route, ProtoJSON, HTTP status/header와 OpenAPI contract를
gRPC-Web descriptor compatibility로 대신 증명하지도 않는다.
현재 installed REST reference는 다음 public 의미를 가진다.
- curated `{ success, data, error, meta }` envelope
- stable frontend error vocabulary와 redaction
- create의 승인된 `200 | 201`
- optional HTTP `ETag`, `304`, `If-Match`, `412`
- `Idempotency-Key`와 effect reconciliation
- browser-facing auth/CORS/CSRF, body ceiling과 mapper
일반 transcoding gateway의 기본 ProtoJSON/status/error 출력은 이 contract와
동일하지 않다. 따라서 현재 reference operation은 curated BFF를 기본 선택으로
유지한다. 제품이 direct gateway를 선택할 때는 이 ADR의 별도 artifact와 public
contract compatibility를 먼저 승인한다.
## 결정
### 1. Public protocol과 adapter를 혼용하지 않는다
지원 가능한 adapter profile은 다음처럼 분리한다.
```text
CURATED_REST_BFF
PROTOBUF_REST_JSON_GATEWAY
GRPC_WEB_BINARY
GRPC_WEB_TEXT
CONNECT_PROTOCOL
```
한 semantic operation의 한 public endpoint는 하나의 exact profile에만
binding한다.
- HTTP media를 보고 runtime이 BFF와 gateway를 자동 선택하지 않는다.
- provider 장애를 이유로 command를 다른 protocol로 자동 replay하지 않는다.
- 같은 path에 BFF envelope와 direct ProtoJSON을 content negotiation으로
섞지 않는다.
- gRPC-Web proxy route를 HTTP/JSON REST route로 간주하지 않는다.
- 내부 gRPC service address, fully-qualified method와 metadata를 browser input으로
받지 않는다.
- shadow read는 secondary 결과를 사용자나 cache에 반영하지 않는다.
public adapter 변경은 동일 backend method를 사용하더라도 API contract migration
이다. N/N-1 route, payload, status, error와 cache evidence 없이 in-place 전환하지
않는다.
### 2. Topology와 현재 기본 선택
현재 기본:
```text
Browser
-> CDN / reverse proxy / WAF
-> same-origin or approved-origin curated BFF
-> public REST contract enforcement
-> application service or internal gRPC service
```
제품 승인 뒤 가능한 direct topology:
```text
Browser
-> CDN / reverse proxy / WAF
-> authentication / authorization edge
-> pinned REST transcoding gateway
-> exact generated route allowlist
-> ProtoJSON codec
-> internal gRPC application service
```
gateway deployable이 독립 process인지 application과 같은 process인지는 이 ADR의
핵심이 아니다. 다음 owner가 논리적으로 분리돼야 한다.
- edge: TLS, origin, request admission, encoded byte/rate ceiling
- gateway: HTTP route, path/query/body mapping, ProtoJSON, status/header projection
- application service: authorization 재검사, validation, transaction,
idempotency, pagination, revision과 domain invariant
- persistence: durable dedupe, cursor/snapshot과 concurrency authority
### 3. Protobuf source of truth와 artifact
`.proto` source와 dependency는 authenticated immutable input이어야 한다.
```text
ProtoContractArtifactV1
artifactId
contractVersion
sourceRepository
sourceCommit
sourceTreeDigest
bufModuleRef
bufModuleCommit
bufModuleDigest
bufLockDigest
bufImageId
bufImageDigest
fileDescriptorSetId
fileDescriptorSetDigest
includeImports
includeSourceInfo
httpRuleSource = PROTO_ANNOTATION | SERVICE_CONFIG
httpRuleArtifactId
httpRuleArtifactDigest
productionBaselineArtifactId
productionBaselineDigest
protoJsonProfileId
generatedGatewayArtifactId
generatedGatewayDigest
generatedOpenApiArtifactId
generatedOpenApiDigest
minimumBackendVersion
minimumGatewayVersion
retirementEpoch | null
owner
```
불변조건:
- moving branch, tag, label이나 `latest`를 production generation input으로 사용하지
않는다.
- Buf Schema Registry를 사용하면 exact module commit/digest를 release에 고정한다.
- `buf.lock`의 direct/transitive dependency commit과 digest를 check-in한다.
- `google/api/annotations.proto`, `http.proto`, well-known type dependency가 실제
descriptor input과 일치해야 한다.
- Buf image와 native `google.protobuf.FileDescriptorSet`을 구분해 저장한다.
- descriptor digest는 exact artifact byte의 integrity/provenance 식별자다.
digest가 다르다는 사실만으로 semantic breaking을 추측하지 않고 compatibility
gate를 별도로 실행한다.
- production server reflection을 source/codegen 공급망으로 사용하지 않는다.
reflection은 승인된 diagnostics profile에서만 사용할 수 있다.
- artifact download는 authenticated channel, integrity verification과 audit
evidence를 가져야 한다.
### 4. Schema evolution과 breaking policy
공통 protobuf governance:
- field number 재사용 금지
- 제거한 field number와 name을 모두 reserve
- package/service/method full name 안정성
- 삭제 대신 deprecate와 retirement window
- proto3 optional/presence와 implicit default 의미 명시
- enum zero value와 unknown numeric value 처리
- 제거한 enum name/number reserve
- oneof absence, 새 case와 unknown case 처리
- map iteration order를 identity, signature와 pagination order로 사용하지 않음
- repeated/map/message depth와 decoded byte ceiling
- Timestamp/Duration valid range, nanos와 timezone projection
- int64/uint64를 JavaScript `number`로 무조건 변환하지 않음
- bytes base64와 maximum decoded length
- `Any`, `Struct`, `Value`는 type allowlist와 semantic validator 없이는 public
contract에 넣지 않음
breaking baseline은 mutable main branch가 아니라 마지막으로 production에
promote된 immutable artifact다.
Buf category:
- generated gateway, generated server/client 또는 여러 언어 source compatibility를
보호하는 기본은 `FILE`이다.
- source compatibility를 의도적으로 별도 관리해도 HTTP/JSON을 제공하는 module은
최소 `WIRE_JSON`을 통과해야 한다.
- JSON을 사용하는 module에서 `WIRE`만 통과한 것을 REST compatibility 증거로
사용하지 않는다.
`google.api.http`와 다른 custom option의 의미는 generic Buf breaking rule만으로
닫히지 않는다. 따라서 다음을 별도 gate로 둔다.
- canonical route manifest semantic diff
- generated OpenAPI compatibility diff
- HTTP method/path/body/query/response body 변경 분류
- additional binding 추가/삭제/충돌 검사
- path escaping/unescaping profile diff
- auth, idempotency, pagination, conditional과 status profile diff
schema wire-safe 변경도 제품 의미, validation ceiling, pagination이나 auth scope를
바꾸면 semantic breaking일 수 있다. Buf success를 mapper/domain compatibility
승인으로 사용하지 않는다.
### 5. Code generation provenance와 reproducibility
generation toolchain:
```text
CodegenToolchainV1
bufCliVersion
protocVersion
plugins[]:
pluginId
pluginVersion
pluginRevision
binaryDigest
optionsDigest
invocationStrategy
protobufRuntimeId
protobufRuntimeVersion
grpcRuntimeVersion
gatewayRuntimeVersion
languageRuntimeVersion
osImageDigest
```
- Buf CLI, protoc, every local/remote plugin과 runtime을 exact version으로 pin한다.
- BSR remote plugin은 upstream version뿐 아니라 repackaging revision도 고정한다.
- version을 생략해 latest plugin을 선택하지 않는다.
- local plugin은 package lock과 executable digest를 함께 기록한다.
- plugin option, managed-mode override, include/exclude path/type와 invocation
strategy를 manifest에 포함한다.
- generated code보다 오래된 incompatible protobuf runtime을 조립하지 않는다.
- `buf.gen.yaml`의 output 경계는 생성 전에 clean되며 generated directory에
hand-written file을 두지 않는다.
CI pipeline:
```text
authenticated immutable source
-> dependency lock verification
-> buf format/lint
-> descriptor/image build
-> breaking check against production baseline
-> HttpRule route manifest generation/diff
-> pinned gateway/OpenAPI/runtime code generation
-> clean regenerate
-> worktree diff == 0
-> generated compile/typecheck/test
-> N/N-1 binary + ProtoJSON fixtures
-> provider conformance
-> artifact sign/attest/SBOM
-> release contract set
```
“Buf를 사용했다”는 이유만으로 deterministic generation이라 부르지 않는다.
동일 input/toolchain에서 clean regenerate diff가 0이고 artifact digest가
일치하는 증거를 남긴다. timestamp, absolute local path와 host-dependent output을
생성물에서 금지하거나 normalize한다.
generated gateway/server/message/OpenAPI는 application/domain model이 아니다.
frontend가 generated OpenAPI client를 선택하더라도 transport adapter 밖으로
generated DTO를 노출하지 않고 VD-24 runtime schema와 mapper를 유지한다.
### 6. ProtoJSON profile
모든 gateway operation은 immutable `ProtoJsonProfile`에 binding한다.
```text
ProtoJsonProfileV1
profileId
specificationRevision
useProtoNames
discardUnknownOnRequest
emitUnpopulated
useEnumNumbers
allowPartial
int64Projection
bytesProjection
nullPresencePolicy
duplicateFieldPolicy
timestampPolicy
durationPolicy
fieldMaskPolicy
anyTypeAllowlist[]
maxDecodedMessageBytes
maxDepth
maxNodes
maxStringBytes
maxRepeatedItems
maxMapEntries
```
reference 기본:
```text
useProtoNames = false
discardUnknownOnRequest = false
emitUnpopulated = false
useEnumNumbers = false
allowPartial = false
int64Projection = DECIMAL_STRING
bytesProjection = STANDARD_BASE64
nullPresencePolicy = PROTOJSON_UNSET
duplicateFieldPolicy = REJECT_OR_PROVIDER_PINNED
```
wire 의미:
- JSON field name은 lowerCamelCase가 기본이며 proto field name 입력 수용 여부와
출력 option을 fixture로 고정한다.
- int64/uint64 출력은 decimal string이다. frontend mapper가 safe integer 범위를
증명하지 않는 한 `number`로 변환하지 않는다.
- bytes는 base64를 decode하기 전 encoded/decoded ceiling을 모두 확인한다.
- enum output은 name이며 unknown numeric value를 application enum으로 추측하지
않는다.
- serializer는 presence가 없는 default field를 기본 생략한다.
- `null`은 일반 field의 명시적 domain null이 아니라 protobuf unset으로
해석될 수 있다. nullable domain 의미는 별도 message/oneof/mapper가 소유한다.
- unknown request field를 production에서 조용히 버리지 않는다.
- response additive rollout은 old frontend가 unknown field를 받는 N/N-1 fixture를
통과한 뒤 writer를 활성화한다.
- duplicate field를 사용하는 client 의미에 의존하지 않는다. selected runtime이
reject하지 않으면 deterministic last-value behavior와 WAF/parser differential
test를 별도 증명한다.
- Timestamp/Duration/FieldMask/wrapper well-known type의 special JSON mapping을
ordinary object로 추측하지 않는다.
gateway decoder success는 semantic validation proof가 아니다. application
service와 frontend boundary는 range, presence, enum/oneof, collection ceiling과
domain invariant를 각각 다시 검증한다.
### 7. HttpRule과 route manifest
기본 source of truth는 RPC의 `google.api.http` annotation이다.
외부 gRPC API service-config YAML은 proto를 직접 변경할 수 없거나 동일 service를
서로 다른 public API로 투영해야 하는 승인된 경우에만 사용한다.
- annotation과 service-config를 같은 RPC에 중복 정의하지 않는 것이 기본이다.
- service-config가 matching annotation을 override할 수 있으므로 artifact digest,
selector, precedence와 generated route 결과를 release에 포함한다.
- config rule의 last-one-wins에 의존해 중복 selector를 숨기지 않는다.
- `generate_unbound_methods`는 기본 false다.
```text
RestGatewayRouteManifestV1
manifestId
protoContractArtifactId
httpRuleArtifactDigest
routes[]:
semanticOperationId
fullyQualifiedService
method
rpcKind
httpMethod
pathTemplate
additionalBindingIndex | null
pathFieldBindings
queryFieldBindings
requestBodyBinding | null
responseBodyBinding | null
protoJsonProfileId
operationProfileId
routeManifestDigest
```
HttpRule 불변조건:
- GET/DELETE operation은 request body를 갖지 않는다.
- path variable은 허용된 non-repeated primitive field에만 binding한다.
- body field는 top-level이며 path field와 중복되지 않는다.
- `body: "*"`이면 path에 포함되지 않은 모든 field가 body로 가고 query
parameter가 없음을 계약한다.
- body를 생략하면 request body가 없고 나머지 field가 query로 가는 것을
계약한다.
- repeated primitive query의 repeated-key 의미와 maximum item을 고정한다.
- nested message query flattening은 approved field set과 depth ceiling을 가진다.
- additional binding 안에 additional binding을 중첩하지 않는다.
- 한 RPC의 additional binding이 다른 RPC route와 충돌하지 않는다.
- reserved path character와 multi-segment parameter는 selected gateway의
`AllExceptReserved` 동등 unescaping profile과 actual fixture를 통과한다.
- caller-provided custom HTTP verb와 arbitrary route를 허용하지 않는다.
PATCH가 FieldMask를 사용하면 gateway 자동-population 여부와 `body: "*"` 예외를
profile에 고정한다. partial update의 최종 authorization, writable field allowlist와
mask validation은 application service가 수행한다.
### 8. OpenAPI contract
generated OpenAPI는 documentation 부산물이 아니라 browser-facing compatibility
artifact다.
```text
GatewayOpenApiArtifactV1
generatorId
generatorVersion
generatorRevision
generatorOptionsDigest
routeManifestDigest
protoJsonProfileId
errorProfileId
statusProfileId
documentDigest
```
- production-stable pipeline은 selected grpc-gateway
`protoc-gen-openapiv2` version을 pin한다.
- 공식 상태가 alpha인 OpenAPI v3 generator는 별도 승인과 toolchain conformance
전까지 production source of truth로 승격하지 않는다.
- generated document를 authenticated immutable artifact로 배포한다.
- generated path, parameter, request/response schema, status와 error가 actual
gateway fixture와 일치해야 한다.
- runtime response rewriter처럼 OpenAPI에 표현되지 않는 변환은 direct gateway의
기본 public contract에서 금지한다.
- custom transformation이 필요하면 curated BFF를 선택하거나 별도 hand-written
contract와 bidirectional conformance를 소유한다.
- OpenAPI client 생성 여부와 무관하게 frontend runtime boundary validation을
제거하지 않는다.
`google.api.HttpBody`는 raw/binary body를 전달할 수 있지만 일반 application
REST에 암묵적으로 사용하지 않는다. 파일 upload/download, Range, presigned URL,
background transfer와 CDN은 VD-12/VD-14의 transfer 계약을 유지한다.
### 9. Gateway provider와 operation definition
```text
RestTranscodingProviderProfileV1
providerId
gatewayImplementation
gatewayVersion
fixedHttpsOrigin
basePathPrefix
routeManifestId
routeManifestDigest
protoContractArtifactId
protoJsonProfileId
openApiArtifactId
authProfileId
credentialsMode
corsProfileId
incomingHeaderProfileId
outgoingHeaderProfileId
pathUnescapingProfileId
errorProfileId
statusProfileId
requestAdmissionProfileId
redirect = ERROR
referrerPolicy
owner
```
```text
ProtobufRestOperationV1
protocol = PROTOBUF_REST_JSON_V1
semanticOperationId
providerId
fullyQualifiedService
method
rpcKind = UNARY
requestMessageId
responseMessageId
protoContractArtifactId
descriptorDigest
routeManifestDigest
httpMethod
pathTemplate
requestBodyBinding | null
responseBodyBinding | null
requestSemanticSchemaId
responseSemanticSchemaId
mapperId
successStatusMediaProfileId
errorProfileId
authProfileId
csrfProfileId
replayPolicy
idempotencyProfileId | null
deadlineProfileId
retryProfileId
paginationProfileId | null
conditionalProfileId | null
serverStateProfileId | null
maxUrlBytes
maxRequestHeaderBytes
maxRequestBodyBytes
maxDecodedRequestBytes
maxResponseHeaderBytes
maxEncodedResponseBytes
maxDecodedResponseBytes
maxResponseItems
owner
```
composition이 거절:
- unknown service/method/message/descriptor
- descriptor, route manifest, ProtoJSON과 OpenAPI digest mismatch
- operation과 provider profile mismatch
- client-streaming 또는 bidirectional RPC
- 별도 streaming ADR 없는 server-streaming RPC
- caller-provided URL, service, method, metadata와 header
- body/query/path/status가 generated route와 다름
- ceiling, deadline, auth, error 또는 owner가 없음
- non-replayable command에 retry
- keyed command에 durable idempotency evidence가 없음
### 10. Unary-only default와 streaming 경계
direct REST Gateway reference는 unary RPC만 허용한다.
- unary request 한 개와 unary response 한 개
- exact terminal HTTP status/media/body
- bounded response decode
grpc-gateway의 server stream은 newline-separated JSON chunk envelope와 terminal
error body를 사용할 수 있다. 이것은 ordinary REST JSON response도, VD-27의
gRPC-Web framing도 아니다.
따라서 다음은 `NOT_SELECTED`다.
- REST server stream
- REST client stream
- REST bidirectional stream
- streaming `HttpBody` file delivery를 ordinary API adapter로 사용
제품이 server stream을 요구하면 chunk media type, message delimiter,
partial-delivery meaning, terminal error, backpressure, sequence/gap/resume,
idle/total deadline과 browser buffering을 별도 ADR과 actual provider evidence로
설계한다. SSE, gRPC-Web, WebSocket 또는 bounded polling과도 요구를 다시
비교한다.
### 11. Direct gateway와 curated BFF 선택
| 요구 | 기본 선택 |
| --- | --- |
| `google.api.http`와 canonical ProtoJSON이 그대로 public contract | direct gateway 가능 |
| simple unary resource operation, 별도 envelope/aggregation 없음 | direct gateway 가능 |
| 현재 `{ success, data, error, meta }` envelope 유지 | curated BFF |
| 제품별 DTO projection, aggregation 또는 여러 backend orchestration | curated BFF |
| cookie session, CSRF와 same-origin session lifecycle | curated BFF |
| stable safe error vocabulary와 PII redaction | curated BFF 권장 |
| custom `201/204`, HTTP `304/412`와 conditional cache transaction | curated BFF 또는 별도 custom gateway |
| idempotency receipt/status/reconcile UX | curated BFF 권장 |
| file transfer, Range, presigned URL, background download와 Image CDN | transfer/BFF 경계 |
| server streaming | 별도 protocol ADR |
direct gateway는 다음을 모두 증명할 때만 선택한다.
- public DTO가 canonical ProtoJSON과 일치
- no custom success envelope
- exact google.api.http route가 product REST route와 일치
- safe error/status mapping이 frontend failure vocabulary와 호환
- edge/gateway/upstream auth owner가 명확
- operation이 unary이고 bounded
- custom header/status/cache 의미가 제한적이고 artifact에 표현 가능
- actual browser/gateway/backend conformance 통과
BFF 뒤에서 internal gRPC를 호출하는 것은 direct gateway가 아니다. public
contract owner는 계속 BFF다.
### 12. Request admission과 semantic validation
request 처리:
```text
TLS/WAF encoded admission
-> fixed route/method
-> origin/auth/CSRF/rate admission
-> header/path/query/body byte ceiling
-> HttpRule binding
-> strict ProtoJSON decode
-> generated request message
-> application semantic validation
-> authorization
-> application service
```
- edge는 encoded request/body/header와 request rate ceiling을 소유한다.
- gateway는 URL/path/query/body mapping과 decoded protobuf message ceiling을
소유한다.
- application service는 generated message를 신뢰하지 않고 domain validation과
authorization을 다시 수행한다.
- path와 body에 같은 field가 중복되거나 충돌하면 fail-closed한다.
- unknown query/body field, malformed percent encoding과 parser differential을
거절한다.
- decompression ratio, nested depth, repeated/map count와 string/bytes length에
별도 ceiling을 둔다.
- error body도 bounded writer와 redaction을 통과한다.
### 13. Success status, body와 media
operation은 exact success matrix를 가진다.
```text
GatewaySuccessProfileV1
allowedHttpStatuses[]
responseMediaTypes[]
bodyPolicy = REQUIRED | EMPTY | PROTOJSON_MESSAGE
responseHeaderAllowlist[]
responseBodyMessageId | null
```
- default transcoded unary success를 임의로 `201` 또는 `204`로 추측하지 않는다.
- `google.protobuf.Empty`의 default `200 {}``204`는 같은 contract가 아니다.
- `201 Created`, `Location`, `204 No Content`가 필요하면 generated OpenAPI,
gateway hook, actual fixture와 rollback을 함께 승인한다.
- upstream metadata로 status를 바꾸는 기능은 registered response type/method와
allowlisted value만 허용한다.
- caller가 `x-http-code` 같은 internal control metadata를 제출하지 못한다.
- status hook 뒤 body가 이미 write된 상태에서 status를 변경하지 않는다.
- response body rewrite가 OpenAPI에 반영되지 않으면 direct public gateway에서
사용하지 않는다.
- HTML proxy error, redirect login page와 unexpected media를 ProtoJSON으로
해석하지 않는다.
현재 curated REST envelope를 direct ProtoJSON으로 조용히 바꾸지 않는다. direct
gateway adoption에는 새 operation/version 또는 증명된 compatibility facade가
필요하다.
### 14. Error contract와 redaction
gateway error profile:
```text
GatewayErrorProfileV1
grpcToHttpStatusMapId
publicEnvelope = GOOGLE_RPC_STATUS | CURATED_APP_FAILURE
safeReasonDomainAllowlist[]
safeDetailTypeAllowlist[]
exposeGrpcMessage = false
exposeUnknownDetails = false
maximumErrorBytes
maximumDetails
```
- gRPC canonical status와 HTTP status mapping을 exact gateway revision에 pin한다.
- routing `404/405/400`과 upstream application error를 구분한다.
- `Status.message`는 machine branch key가 아니다.
- arbitrary `Status.message`, stack, SQL/vendor copy, internal address와 raw
`Any.details`를 frontend에 노출하지 않는다.
- frontend branch는 registered stable reason/code/category만 사용한다.
- `ErrorInfo`를 사용하면 `(reason, domain)`과 approved metadata key를 versioned
vocabulary로 관리한다.
- authorization detail은 resource 존재 여부나 policy reason을 누출하지 않는다.
- malformed/oversize upstream error는 safe generic provider failure로 닫는다.
- unary custom error handler와 routing error handler가 actual OpenAPI/fixture와
일치해야 한다.
- stream error handler는 unary handler와 다르므로 streaming이 선택되기 전
production guarantee로 표시하지 않는다.
partial success/error는 기본 거절한다. bulk partial semantics가 필요하면
long-running operation이나 별도 result contract를 선택한다.
### 15. Authentication, authorization, CORS와 CSRF
gateway는 authorization authority를 자동 제공하지 않는다.
- edge 또는 gateway가 credential을 검증해도 application service가 operation과
resource authorization을 다시 수행한다.
- bearer profile은 issuer, audience, signature algorithm, time claims, revocation과
tenant binding을 검증한다.
- cookie profile은 `Secure`, `HttpOnly`, explicit `SameSite`, host/path scope와
unsafe method CSRF를 함께 승인한다.
- credentialed CORS에 wildcard origin을 사용하지 않는다.
- exact origin/method/header/expose 목록과 bounded preflight cache를 fixture로
검증한다.
grpc-gateway의 incoming `Authorization`은 upstream gRPC metadata로 전달될 수
있다. 이 동작을 다음처럼 다룬다.
- public bearer를 upstream service가 검증하는 profile인지 명시
- edge가 identity를 변환하면 original credential 전달/제거 owner를 별도 설계
- 외부 caller가 내부 principal, tenant, role, trace와 policy header를 spoof하지
못하도록 edge에서 제거
- incoming/outgoing header matcher는 closed allowlist
- hop-by-hop, cookie, raw credential, internal debug와 arbitrary `grpc-*`
metadata를 forwarding하지 않음
- auth attach 뒤 final origin/path/method/body digest와 registry binding 재검증
current cookie/same-origin이나 curated auth/error/meta 요구가 크면 BFF를 선택한다.
### 16. Deadline과 cancellation
```text
browser total deadline
-> edge admission
-> gateway request context
-> upstream gRPC deadline
-> application/downstream deadline propagation
```
- gRPC는 deadline이 기본으로 자동 설정된다고 가정하지 않는다.
- frontend total deadline 안에서 edge/gateway/upstream phase가 더 짧은 ceiling을
가진다.
- retry/backoff가 total deadline을 늘리지 않는다.
- gateway는 remaining budget을 upstream deadline으로 전달하고 queue/connection
time을 제외하지 않는다.
- browser AbortSignal/navigation/logout와 connection close를 구분된 safe
cancellation reason으로 정규화한다.
- HTTP disconnect가 upstream gRPC cancel과 application work stop으로 이어지는지
actual provider에서 검증한다.
- server handler와 spawned/downstream work는 cancellation을 주기적으로 확인하고
불필요한 계산/I/O를 중단한다.
- deadline/cancel 뒤 timer, request body, response writer와 upstream call을
정리한다.
cancellation은 이미 commit된 effect를 rollback하지 않는다. client와 server의
success 판단이 다를 수 있으므로 command는 cancellation 결과만으로 effect
없음을 선언하지 않는다.
### 17. Retry와 idempotency
semantic replay policy:
```text
SAFE
IDEMPOTENT
KEYED_COMMAND
NON_REPLAYABLE
```
- gateway의 모든 upstream gRPC call이 HTTP POST라는 이유로 retry-safe라
간주하지 않는다.
- frontend, CDN/proxy, gateway, gRPC client와 service retry 중 한 owner만 physical
retry를 수행한다.
- automatic retry 기본 대상은 bounded unary safe read다.
- transactional sequence, `ABORTED`, validation, auth와 non-replayable command를
transport가 자동 retry하지 않는다.
- response header/body가 시작된 뒤 retry하지 않는다.
- attempt count, backoff, sleep, elapsed와 total deadline ceiling을 둔다.
idempotency transport는 operation마다 하나를 선택한다.
```text
HTTP_IDEMPOTENCY_KEY
PROTO_REQUEST_ID
NONE
```
- 현재 curated REST의 `Idempotency-Key`를 direct gateway가 사용하면 exact
incoming metadata mapping과 upstream binding을 정의한다.
- protobuf `request_id` field를 사용하면 HttpRule body/query 위치, UUID format,
payload fingerprint와 replay window를 정의한다.
- 두 identity를 동시에 받아 어느 값을 authority로 쓸지 추측하지 않는다.
- gateway process-local map/lock을 idempotency store로 사용하지 않는다.
- principal/tenant + semantic operation/version + key + canonical request
fingerprint를 durable store에 binding한다.
- same key/same fingerprint replay는 authoritative prior result/receipt를
반환한다.
- same key/different fingerprint는 stable conflict다.
- commit 뒤 response loss는 새 effect를 만들지 않는다.
- `EFFECT_UNKNOWN`은 새 key나 다른 protocol retry가 아니라 status/reconcile
endpoint로 확인한다.
- retention은 frontend recovery window보다 길고 quota/abuse policy를 가진다.
### 18. Pagination
HttpRule/gateway는 protobuf field를 HTTP query/body로 옮길 뿐 pagination 의미를
구현하지 않는다.
paginated list는 첫 public version부터 다음을 포함한다.
```text
ListRequest
pageSize
pageToken
ListResponse
items[]
nextPageToken
```
application/backend 책임:
- finite default와 maximum page size
- negative size rejection과 oversized coercion/approved policy
- opaque URL-safe page token
- principal/tenant/filter/sort/contract/snapshot binding
- token integrity, expiry와 key rotation
- stable total ordering과 immutable unique tie-breaker
- insertion/deletion 중 snapshot or documented consistency
- duplicate/gap/loop 방지
- next token empty만 end-of-collection 의미
- page token이 authorization을 대체하지 않음
unpaginated RPC에 field만 추가하고 default response를 일부로 줄이는 변경은
behavioral breaking이다. 현재 reference array response를 page object로 바꾸려면
새 operation/schema와 N/N-1 migration을 사용한다.
frontend는 arbitrary next URL을 따라가지 않고 opaque token만 registered query
input에 투영한다. raw token을 log, analytics와 persistent cache에 넣지 않는다.
### 19. ETag, revision과 HTTP conditional
다음 두 profile을 구분한다.
```text
PROTO_RESOURCE_ETAG
HTTP_REPRESENTATION_CONDITIONAL
```
`PROTO_RESOURCE_ETAG`:
- resource/request message의 `string etag` field
- server-owned output
- strong/weak meaning 명시
- mismatch는 selected gRPC status, 보통 `ABORTED`
- ProtoJSON body/query field로 왕복
`HTTP_REPRESENTATION_CONDITIONAL`:
- HTTP response `ETag`
- request `If-None-Match` 또는 `If-Match`
- unchanged read의 `304` empty body
- failed write precondition의 approved `412 | 409`
- exact principal/tenant-visible representation과 encoding variant binding
- `Cache-Control`, `Vary`, CORS expose/preflight와 frontend cache transaction
proto `etag` field가 존재한다고 gateway가 HTTP `ETag`, `304` 또는 `If-Match`
자동 제공한다고 간주하지 않는다. body ETag를 HTTP validator로 투영하려면
allowlisted response header hook, request header mapping, status conversion,
generated/curated OpenAPI와 actual cache fixture가 필요하다.
현재 REST reference의 TanStack/application conditional transaction은
`HTTP_REPRESENTATION_CONDITIONAL`이다. direct gateway가 이 의미를 정확히
구현하지 않으면 curated BFF를 유지한다.
frontend는 validator와 mapped cached value의 scope/query identity/representation
version/cache revision이 모두 일치할 때만 304를 success로 처리한다. raw validator를
log, diagnostics와 metric label에 넣지 않는다.
### 20. Cache와 server state
gateway adoption이 frontend query identity를 바꾸지 않게 semantic operation
input을 canonical source로 유지한다.
- generated path/query serializer와 query-key codec가 같은 validated application
input에서 파생
- ProtoJSON DTO와 generated message를 TanStack Query cache에 저장하지 않음
- schema/semantic validation과 mapper가 끝난 immutable application projection만
저장
- auth/session/tenant generation이 cache scope를 소유
- late response와 retry result는 generation fence 뒤 cache에 들어가지 않음
- protocol migration 중 BFF와 gateway 결과를 같은 key에 섞지 않음
- pagination token과 ETag는 domain resource data와 별도 bounded sidecar owner
- mutation commit 뒤 exact invalidation/reconcile owner 필요
### 21. Observability, privacy와 abuse control
허용 dimension:
```text
semantic operation ID
gateway/provider profile ID
proto contract artifact version
route manifest version
ProtoJSON profile ID
status group / safe error code
attempt bucket
duration and size bucket
admission/cancellation stage
```
금지:
- raw Authorization/cookie/CSRF/idempotency key
- raw path/query/body와 protobuf message
- cursor/page token/ETag/revision
- `Status.message`, unknown details와 validation value
- principal, tenant, resource ID와 IP의 unbounded label
- descriptor/proto source URL credential
gateway는 operation별 request rate, concurrent upstream call, body/response byte,
CPU-heavy JSON decode, error volume와 upstream saturation ceiling을 가진다.
최소 metric:
- route admission/rejection
- ProtoJSON malformed/unknown/oversize
- descriptor/route/OpenAPI artifact mismatch
- gRPC→HTTP status/error category
- deadline/cancel propagation stage
- retry attempts/amplification
- idempotency replay/conflict/unknown effect
- page token invalid/expired
- conditional hit/miss/precondition failure
- provider/browser conformance version
### 22. Provider conformance와 promotion evidence
#### Deterministic
- Buf format/lint/breaking
- exact production baseline
- source/dependency/image/descriptor/toolchain/generated digest
- clean regenerate diff 0
- gencode/runtime compatibility
- generated import boundary와 SBOM
- HttpRule custom-option semantic diff
#### Contract
- descriptor N/N-1 binary fixture
- ProtoJSON scalar/presence/null/unknown/enum/int64/bytes/time matrix
- method/path/body/query/additional binding
- path percent-encoding and `AllExceptReserved` behavior
- generated OpenAPI vs actual request/response
- exact success/media/status/body
- safe unary/routing error mapping
- auth/header allowlist
- idempotency/pagination/conditional fixtures
#### Integration/browser
- actual gateway version + actual gRPC backend
- Chromium/Firefox/WebKit fetch
- CORS/preflight, bearer 또는 cookie+CSRF
- reverse proxy/CDN/WAF body and header ceiling
- AbortSignal/disconnect/deadline propagation
- navigation/logout/account switch
- HTTP/1.1, HTTP/2와 intermediary error behavior
#### Fault/security
- malformed/oversize/truncated JSON
- unknown/duplicate field와 parser differential
- route collision, encoded slash와 path traversal
- spoofed identity/internal control header
- upstream unavailable/deadline/status mismatch
- error detail/stack/PII redaction
- response started 뒤 failure/retry
- commit 뒤 response loss와 idempotency reconciliation
- invalid/expired/wrong-scope page token
- body ETag와 HTTP conditional 혼동 방지
#### Performance/operations
- bounded JSON transcoding CPU/memory
- maximum request/response와 concurrent load
- slow client/upstream and queue saturation
- canary, kill switch와 traffic admission
- coherent proto/gateway/OpenAPI/frontend/backend rollback
- generated/proxy/dependency removal drill
fake gateway, generated compile과 OpenAPI 생성 success만으로 actual provider
conformance를 완료 처리하지 않는다.
### 23. Rollout
1. backend/product owner가 direct gateway가 필요한 operation과 BFF 대비 이점을
승인한다.
2. authenticated proto source, exact production baseline과 artifact owner를
확정한다.
3. Buf/codegen toolchain, breaking category와 HttpRule source를 pin한다.
4. descriptor, route manifest, ProtoJSON profile와 OpenAPI artifact를 생성한다.
5. provider/gateway version, auth/error/status/deadline ceiling을 승인한다.
6. unary safe read 한 개로 reference conformance harness를 만든다.
7. frontend adapter를 `AVAILABLE_NOT_COMPOSED`로 판정한다.
8. product composition 뒤 `TrafficAdmission=DISABLED`로 배포한다.
9. internal/staging shadow read는 cache/UI에 반영하지 않고 BFF result와 semantic
diff를 관찰한다.
10. actual browser canary를 낮은 비율로 시작한다.
11. error/latency/schema/cache SLO와 artifact digest가 정상일 때 단계적으로
확대한다.
12. keyed command는 durable dedupe/reconcile fault evidence 뒤 별도 canary한다.
13. server stream은 계속 `NOT_SELECTED`다.
한 release에서 proto writer, gateway route, error envelope와 frontend consumer를
동시에 breaking 전환하지 않는다. additive reader-first/writer-later와 N/N-1
window를 사용한다.
### 24. Rollback과 removal
rollback:
- 신규 gateway operation traffic admission 중지
- in-flight safe read cancel
- command는 cancel 뒤 authoritative receipt/status로 effect reconcile
- gateway-scoped query/validator/pagination sidecar clear 또는 invalidate
- last-known-good proto/descriptor/route/OpenAPI/gateway/backend/frontend artifact를
coherent하게 복구
- approved BFF read fallback은 새 logical request와 독립 total deadline으로만
실행
- command를 BFF나 gRPC-Web로 자동 replay하지 않음
removal:
1. operation traffic과 route retirement window 시작
2. N/N-1 frontend/backend/gateway 소비자를 확인
3. route manifest와 traffic admission에서 제거
4. frontend operation/schema/mapper/cache profile 제거
5. generated gateway/OpenAPI/message output 제거
6. gateway route, proxy/CORS/auth/config 제거
7. plugin/runtime/package와 SBOM entry 제거
8. proto RPC는 다른 소비자가 없고 deprecation window가 끝난 뒤 제거
9. message field/enum number와 name은 제거 시 reserve
10. descriptor/baseline artifact는 audit/retention 정책에 따라 보존
11. build, bundle, dependency, provider probe와 removal drill 통과
### 25. Backend/provider handoff checklist
- [ ] proto source/module owner와 immutable production baseline이 있다.
- [ ] `buf.lock`, Buf image, native descriptor와 digest가 release에 binding됐다.
- [ ] Buf/protoc/plugin version/revision/options/runtime이 pin됐다.
- [ ] `FILE` 또는 승인된 최소 `WIRE_JSON` breaking gate가 있다.
- [ ] HttpRule custom-option와 generated OpenAPI semantic diff gate가 있다.
- [ ] ProtoJSON profile과 N/N-1 fixture가 있다.
- [ ] direct gateway와 curated BFF 중 operation별 public owner가 하나다.
- [ ] exact unary service/method/route/status/media/error가 allowlist됐다.
- [ ] bearer 또는 cookie+CSRF/CORS provider evidence가 있다.
- [ ] incoming/outgoing header와 internal identity spoofing 방지가 검증됐다.
- [ ] edge/gateway/upstream byte/rate/depth/count ceiling이 있다.
- [ ] total deadline, browser abort와 downstream cancellation evidence가 있다.
- [ ] retry owner가 하나이고 command idempotency/reconcile가 durable하다.
- [ ] pagination을 선택했다면 token/snapshot/authorization contract가 있다.
- [ ] conditional을 선택했다면 body ETag와 HTTP validator profile이 분리됐다.
- [ ] generated OpenAPI와 actual gateway wire fixture가 일치한다.
- [ ] actual browser/gateway/backend canary, kill switch와 rollback drill이 통과했다.
- [ ] generated code, proxy route와 dependency removal drill이 통과했다.
## 규범 기준
- [Protocol Buffers ProtoJSON format](https://protobuf.dev/programming-guides/json/)
- [Protocol Buffers proto3 language guide](https://protobuf.dev/programming-guides/proto3/)
- [Protocol Buffers cross-version runtime guarantee](https://protobuf.dev/support/cross-version-runtime-guarantee/)
- [Buf breaking-change detection](https://buf.build/docs/breaking/)
- [Buf breaking rules and categories](https://buf.build/docs/breaking/rules/)
- [Buf code generation](https://buf.build/docs/generate/)
- [Buf `buf.gen.yaml` v2](https://buf.build/docs/configuration/v2/buf-gen-yaml/)
- [Buf dependency management](https://buf.build/docs/bsr/module/dependency-management/)
- [Buf FileDescriptorSet](https://buf.build/docs/bsr/module/descriptor/)
- [Google API HTTP and gRPC transcoding](https://google.aip.dev/127)
- [Google API errors](https://google.aip.dev/193)
- [Google API request identification](https://google.aip.dev/155)
- [Google API pagination](https://google.aip.dev/158)
- [Google API resource freshness validation](https://google.aip.dev/154)
- [Google API `HttpRule`](https://docs.cloud.google.com/endpoints/docs/grpc-service-config/reference/rpc/google.api)
- [gRPC-Gateway introduction](https://grpc-ecosystem.github.io/grpc-gateway/docs/tutorials/introduction/)
- [gRPC-Gateway FAQ](https://grpc-ecosystem.github.io/grpc-gateway/docs/faq/)
- [gRPC-Gateway customization](https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/customizing_your_gateway/)
- [gRPC-Gateway PATCH](https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/patch_feature/)
- [gRPC-Gateway HttpBody](https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/httpbody_messages/)
- [gRPC-Gateway OpenAPI v3 status](https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/openapi_v3/)
- [gRPC deadlines](https://grpc.io/docs/guides/deadlines/)
- [gRPC cancellation](https://grpc.io/docs/guides/cancellation/)
- [gRPC status codes](https://grpc.io/docs/guides/status-codes/)
## 완료 기준
- Protobuf source/dependency/descriptor/toolchain/generated artifact의 provenance와
digest가 production baseline에 binding된다.
- Buf binary/source compatibility와 HttpRule/OpenAPI semantic compatibility를
별도 gate로 검증한다.
- ProtoJSON field/int64/bytes/enum/null/presence/unknown 의미가 exact profile로
닫힌다.
- direct REST Gateway와 gRPC-Web/curated BFF가 서로 다른 protocol artifact와
state machine을 가진다.
- 현재 installed REST reference는 별도 product 승인 전 curated BFF를 유지한다.
- direct gateway operation은 unary, fixed route/service/method와 bounded
request/response에만 compose된다.
- status/body/error/auth/header/deadline/cancel/retry가 actual provider fixture와
일치한다.
- gateway가 idempotency, pagination, revision과 authorization authority를
대신한다고 표시하지 않는다.
- body ETag와 HTTP conditional semantics를 혼동하지 않는다.
- generated OpenAPI와 actual wire contract drift가 promotion을 차단한다.
- actual browser/gateway/backend conformance, canary, kill switch, coherent rollback과
removal drill이 통과한다.
- schema/codegen implementation 전에는 `DESIGNED_NOT_IMPLEMENTED`, product 선택
전에는 REST Gateway를 `NOT_SELECTED`로 유지한다.