# 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`로 유지한다.