Files
llm-wiki/raw/branch-notes/feature-frontend-multi-protocol-api-transport-contract.md

346 lines
32 KiB
Markdown

---
title: branch / feature-frontend-multi-protocol-api-transport-contract
source_type: branch-note
status: raw
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-031
kind: project-work-item
project: ca-skeleton-frontend-operational-contract
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-031
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CAPABILITY-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-PROTOCOL-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-VALIDATION-001@1]
refines: []
overrides: []
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-005, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-006, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-007]
imports: [FE-GATE-030@1, FE-OC-006@1, FE-OC-007@1, FE-OC-008@1, FLOW-FE-RESP-004@1, FLOW-FE-RESP-005@1, FLOW-FE-RESP-006@1]
delegates: [DELEG-FE-010@1]
accepts_delegations: []
contract_packet: 1
branch: feature-frontend-multi-protocol-api-transport-contract
parent_branch:
related_projects: [ca-skeleton-frontend, ca-skeleton]
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
tags: [branch, ca-skeleton-frontend, api, protocol, graphql, grpc-web]
created: 2026-07-28
target_merge:
status_label: in-progress
---
# branch: feature-frontend-multi-protocol-api-transport-contract
<!-- section-id: branch-parent -->
## 부모 (필수)
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
형제 branch (같은 부모, 이번 확장에서 함께 생성):
- [[raw/branch-notes/feature-frontend-binary-file-io-store-contract]]
- [[raw/branch-notes/feature-frontend-cache-tier-cross-tab-invalidation-contract]]
- [[raw/branch-notes/feature-frontend-large-object-transfer-contract]]
- [[raw/branch-notes/feature-frontend-realtime-subscription-lifecycle-contract]]
- [[raw/branch-notes/feature-frontend-background-execution-worker-contract]]
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `2`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: protocol별 성공/실패 정규화와 gateway fallback fixture가 통과한다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CAPABILITY-001@1` | 신규 runtime capability 6종은 FE-REG-CAPABILITY flag로 default OFF이며 활성화는 owner·gate·runbook을 동반한다 | `CAP_FE_ALT_PROTOCOL` 을 이 branch 가 소유하고 default OFF 로 유지한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-PROTOCOL-001@1` | transport default는 REST이고 GraphQL·gRPC-Web·Connect-Web은 FE-REG-API의 protocol 필드로 opt-in하며 미지원 환경은 REST gateway로 fallback한다 | `FE-REG-API.protocol` 값에 따른 adapter 선택 규칙에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-VALIDATION-001@1` | boundary runtime validation은 Zod schema로 수행한다 | codec 디코드 결과도 예외 없이 스키마 검증을 거친다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
| D1 | 이 branch 는 **신규 port 를 정의하지 않는다**. GraphQL·gRPC-Web·Connect-Web adapter 는 기존 `ResourceQueryPort`/`ResourceCommandPort` 를 구현한다 | `refines DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-PROTOCOL-001@1` | **`UNSUPPORTED_DECISION`** — 조사 후에도 외부 근거 없음. trade-off: 프로토콜별 port 를 만들면 use case 가 프로토콜을 알게 되어, backend 가 REST 에서 gRPC 로 옮길 때 use case 를 다시 써야 한다. 비용은 프로토콜 고유 기능(양방향 스트림 등)이 필요해질 때 port 를 새로 뚫어야 하는 것 | `proposed` |
| D2 | transport status 만으로 성공을 판정하지 않는다. protocol 별 성공 판정 함수가 별도로 존재한다 | `local` | `raw/official-docs/graphql-over-http-draft-status-errors.md#C3` (상태 코드와 무관하게 본문 처리), `raw/official-docs/grpc-connect-status-codes-error-model.md#C3` (status 는 transport 와 별도), `#C5` (Connect 는 non-200) | `proposed` |
| D3 | GraphQL 응답에 `errors` 가 있으면 `PARTIAL_RESULT_FAILURE` 로 정규화한다. **규격은 이를 "successful execution" 이라 부르므로 이 결정은 의도적 이탈이다** | `local` | `raw/official-docs/graphql-over-http-draft-status-errors.md#C4` (규격의 명명), `#C6` (`data: null` 이면 errors 가 반드시 있음) | `proposed` |
| D4 | GraphQL 성공 판정은 상태 코드가 아니라 본문의 `data`·`errors` 구조로 한다. **`200` 만 검사하지 않는다** | `local` | `raw/official-docs/graphql-over-http-draft-status-errors.md#C1` (data+errors 는 `294` 권고), `#C2` (data 있으면 `2xx`), `#C3` (상태 코드 무관 처리) | `proposed` |
| D6 | `grpc-status` → 재시도 가능 여부 매핑은 **우리 프로젝트 결정**으로 등록하고 그 근거를 남긴다. 규격 인용으로 대신하지 않는다 | `local` | `raw/official-docs/grpc-connect-status-codes-error-model.md#C1` (코드 목록은 확정), `#C2` (재시도 판정은 애플리케이션 몫이라고 규격이 명시) | `proposed` |
| D7 | gRPC-Web adapter 와 Connect adapter 를 **분리한다**. 성공 판정 코드를 공유하지 않는다 | `local` | `raw/official-docs/grpc-connect-status-codes-error-model.md#C5` (Connect 오류는 non-200), `#C6` (Connect 는 trailer 미사용), `#C7` (gRPC-Web 은 다른 프로토콜) | `proposed` |
**deferred (이번 회차 조사 범위 밖)**: D8 — persisted-document ID 기반 GraphQL 요청의 전송 방식(GET vs POST, 캐시 가능성, media type 협상). GraphQL over HTTP draft §5·§6.2 를 따로 읽어야 하며, 이번 조사는 §6.4 상태 코드·오류 처리에 한정했다.
<!-- section-id: declared-overrides -->
### 선언한 예외
해당 없음.
<!-- GENERATED: artifact-imports:start -->
### 가져온 artifact 계약
| Artifact Ref | Owner | Producer | Schema Ref |
|---|---|---|---|
<!-- GENERATED: artifact-imports:end -->
<!-- GENERATED: project-contract-imports:start -->
## 가져온 프로젝트 계약
| Ref | Owner | 요약 | Branch 적용 |
|---|---|---|---|
| `FE-GATE-030@1` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | protocol 별 성공/실패 정규화 fixture 가 실패하면 merge 를 MUST 차단 | 이 branch 가 owner 로서 fixture 와 report 를 산출 |
| `FE-OC-006@1` | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | 모든 HTTP는 shared client를 MUST 통과하고 timeout·abort·response parsing을 page에서 구현하면 안 됨 | protocol adapter 도 shared client 위에 얹힌다 |
| `FE-OC-007@1` | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | JSON envelope와 payload는 boundary에서 runtime schema를 MUST 통과 | 디코드 후 검증을 `DELEG-FE-010` 으로 위임 |
| `FE-OC-008@1` | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] | 모든 failure는 stable frontend error kind로 MUST 정규화하고 raw body·stack을 UI에 노출하면 안 됨 | protocol 실패 3종의 정규화 대상 |
<!-- GENERATED: project-contract-imports:end -->
<!-- GENERATED: received-delegations:start -->
### 수신한 위임
없음. 이 branch 는 `DELEG-FE-010` 의 delegator 다.
<!-- GENERATED: received-delegations:end -->
<!-- GENERATED: flow:start -->
### 가져온 흐름 단계
| Stage Ref | Order | Owner | Input | Action | Output |
|---|---:|---|---|---|---|
| `FLOW-FE-RESP-004@1` | 4 | `feature-runtime-schema-validation-contract` | unvalidated JSON | envelope 공유 스키마 검증 | discriminated envelope |
| `FLOW-FE-RESP-005@1` | 5 | `feature-runtime-schema-validation-contract` | discriminated envelope | success/failure 분기 검증 | 분기 확정 envelope |
| `FLOW-FE-RESP-006@1` | 6 | `feature-runtime-schema-validation-contract` | 분기 확정 envelope | payload per-operation 스키마 검증 | 검증된 payload(deep clone) |
<!-- GENERATED: flow:end -->
<!-- section-id: branch-goal -->
## 목표
GraphQL·gRPC-Web·Connect-Web adapter 가 기존 `ResourceQueryPort`/`ResourceCommandPort` 를 구현하도록 고정하고, protocol 별 성공/실패 판정을 정규화된 failure 로 매핑한다. 이 계약이 없으면 `200 OK` + `errors[]` 응답이 success 로 반환되어 빈 화면이 정상처럼 보이고, HTTP 200 + `grpc-status: 13` 이 성공으로 처리된다.
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- `FE-REG-API``protocol`·`operationRef`·`transferMode` 소비와 adapter 선택
- GraphQL codec — persisted-document ID 기반 요청, `errors[]` 처리
- gRPC-Web·Connect-Web codec — protobuf 인코딩/디코딩, `grpc-status` ↔ 정규화 kind 매핑
- REST gateway fallback (`disabledFallback: degraded-alternative`)
- 신규 실패 3종 정규화: `PROTOCOL_STATUS_MISMATCH`·`CODEC_DECODE_FAILURE`·`PARTIAL_RESULT_FAILURE`
- `CAP_FE_ALT_PROTOCOL` capability 행 소유
### 제외 범위
- **use case, domain model, business rule** — adapter 계약까지만 정의한다
- **신규 port 정의** — 0개가 이 branch 의 설계 결론이다. 프로토콜이 application 에 새 인터페이스로 새면 `FE-D010` dependency inversion 이 무너진다
- GraphQL 스키마 설계, protobuf 서비스·메시지 정의
- 스키마 생성 파이프라인의 SSOT — `FE-Q-013`
- 디코드 이후 payload 의 runtime schema 검증 — `DELEG-FE-010` 로 [[raw/branch-notes/feature-runtime-schema-validation-contract]] 에 위임
- 스트림 protocol(`sse`·`websocket`·`poll`) — [[raw/branch-notes/feature-frontend-realtime-subscription-lifecycle-contract]] 소유
## 근거 (필수, 최소 1개+)
| Source | 정당화하는 결정 |
|---|---|
| `[[docs/superpowers/specs/2026-07-28-ca-skeleton-frontend-runtime-adapter-features-design]]` §5.2·§6.2 | 신규 port 0개 결론과 `FE-REG-API` 확장 |
| [[raw/official-docs/zod-runtime-schema-validation-official]] | 디코드 후 검증의 상위 근거 |
| [[raw/official-docs/graphql-over-http-draft-status-errors]] | D3 `errors` 응답 정규화가 규격 이탈임을 밝히는 근거 · D4 상태 코드가 아니라 본문으로 성공 판정 |
| [[raw/official-docs/grpc-connect-status-codes-error-model]] | D2 protocol 별 성공 판정 분리 · D6 재시도 판정이 우리 결정인 근거 · D7 gRPC-Web 과 Connect 를 분리하는 근거 |
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §2.1.4·§7.3·§8.2 | 응답 흐름 8단계와 실패 정규화 |
**근거 등급 경계**: 2026-07-28 `/branch-spec` 조사로 세 프로토콜의 **오류 표현 방식**은 규격 근거를 확보했다. 다만 두 가지를 구분해야 한다. (1) gRPC 코드 목록(0~16)은 규격이 확정해 주지만, **어떤 코드가 재시도 가능한지는 규격이 명시적으로 애플리케이션에 위임한다**`grpc-connect-status-codes-error-model#C2`. 즉 매핑표는 "규격 확인 후 채운다" 가 아니라 "우리가 정하고 근거를 남긴다" 다. (2) GraphQL over HTTP 는 **draft** 이며, 그 규격은 field error 상황을 오히려 "successful execution" 이라 부른다(`graphql-over-http-draft-status-errors#C4`). 우리 `PARTIAL_RESULT_FAILURE` 결정은 규격 준수가 아니라 **의도적 이탈**이다. 신규 port 0개(D1)는 여전히 외부 근거가 없다.
## TODO
- [ ] `FE-REG-API.protocol` 별 adapter 선택 규칙 확정 — 등급: `planned`
- [ ] protocol 별 성공 판정 함수 분리 — 등급: `planned`
- [ ] GraphQL 본문 `errors``PARTIAL_RESULT_FAILURE` 매핑 (상태 코드 무관, `294` 포함) — 등급: `planned`
- [ ] `grpc-status` → 정규화 kind 매핑표를 **우리 결정으로 작성 후 backend 소유자와 대조** — 등급: `planned`
- [ ] gRPC-Web adapter 와 Connect adapter 분리 유지 fixture — 등급: `planned`
- [ ] codec decode 실패 → `CODEC_DECODE_FAILURE` — 등급: `planned`
- [ ] REST gateway fallback 경로 — 등급: `planned`
- [ ] protocol 차원과 gateway fallback 발생률 telemetry 등록 (관심사 7 should-fix) — 등급: `planned`
- [ ] `FE-GATE-030` protocol mapping report 산출 — 등급: `planned`
## 진행 중 메모
이 branch 의 가치는 "무엇을 추가했는가" 보다 "무엇을 추가하지 않았는가" 에 있다. 신규 port 0개라는 결론이 유지되어야 backend 가 REST 에서 gRPC 로 옮겨갈 때 use case 를 다시 쓰지 않는다. 리뷰 시 port 가 늘어나 있으면 그 자체가 회귀 신호다.
## 결정 사항
- 2026-07-28: 신규 port 0개 / 이유: 프로토콜은 registry 데이터이지 타입이 아니며, port 로 새면 dependency inversion 이 무너짐 / 검토한 대안: `GraphQLPort`·`GrpcWebPort` 분리 / 근거: 설계문서 §5.2
- 2026-07-28: `errors` 있는 응답을 실패로 정규화 / 이유: 부분 데이터를 성공으로 취급하면 빈 화면이 정상처럼 보임 / 검토한 대안: 부분 성공 상태 신설 / 근거: **규격 이탈을 자각한 project decision**. GraphQL over HTTP draft 는 이를 "successful execution" 이라 부른다(`graphql-over-http-draft-status-errors#C4`)
- 2026-07-28: 성공 판정에서 상태 코드를 빼고 본문 구조만 봄 / 이유: 규격이 data+errors 에 `294` 를 권고하고 클라이언트에게 상태 코드와 무관한 처리를 요구함 / 검토한 대안: `200`·`294` 를 모두 허용 목록에 넣기 / 근거: `graphql-over-http-draft-status-errors#C1`·`#C3`. 허용 목록 방식은 draft 가 코드를 바꾸면 다시 깨진다
- 2026-07-28: gRPC-Web 과 Connect adapter 분리 / 이유: Connect 는 오류를 non-200 으로 보내고 trailer 를 쓰지 않아 판정 규칙이 정반대 / 검토한 대안: 공통 gRPC 계열 adapter / 근거: `grpc-connect-status-codes-error-model#C5`·`#C6`·`#C7`
<!-- section-id: decision-evidence -->
## 결정-근거 매핑
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | 신규 port 0개 | 항상. 프로토콜 고유 기능(gRPC 양방향 스트림 등)이 use case 레벨에 필요해지면 재검토 | 없음 — 조사 후에도 외부 근거 없음 | `UNSUPPORTED_DECISION` | 추상화가 새는 프로토콜 기능이 있을 수 있음 |
| D2 | protocol 별 성공 판정 함수 분리 | 항상. 세 프로토콜의 성공 신호 위치가 서로 달라 예외가 없다 | `graphql-over-http-draft-status-errors.md#C3`, `grpc-connect-status-codes-error-model.md#C3`·`#C5` | `official-reference` | 판정 로직이 프로토콜마다 흩어져 중복될 수 있음 |
| D3 | `errors` 있으면 `PARTIAL_RESULT_FAILURE` | 항상. 제품이 부분 데이터를 의미 있게 쓸 수 있으면 재검토 | `graphql-over-http-draft-status-errors.md#C4`(규격은 성공이라 부름), `#C6` | `project decision`**규격 이탈을 자각한 선택** | 일부 필드만 실패한 응답을 통째로 버린다. 규격을 따르는 다른 클라이언트와 동작이 달라진다 |
| D4 | 상태 코드가 아니라 본문으로 성공 판정 | 항상 | `graphql-over-http-draft-status-errors.md#C1`·`#C2`·`#C3` | `official-reference` (draft) | draft 라 `294` 권고가 바뀔 수 있다. 다만 "본문으로 판정" 은 코드가 바뀌어도 유효 |
| D6 | 재시도 매핑을 우리 결정으로 등록 | 항상. 규격이 판단을 위임했으므로 위임을 받은 쪽이 근거를 남겨야 한다 | `grpc-connect-status-codes-error-model.md#C1`, `#C2` | `official-reference`(위임 사실) + `project decision`(매핑 내용) | 매핑이 backend 의 코드 사용 관습과 어긋나면 재시도가 과하거나 부족해진다 |
| D7 | gRPC-Web 과 Connect adapter 분리 | 항상. 두 프로토콜의 오류 위치가 정반대다 | `grpc-connect-status-codes-error-model.md#C5`, `#C6`, `#C7` | `official-reference` | 유사한 코드가 두 벌 생긴다. 공유하려는 리팩터가 나중에 회귀를 만든다 |
<!-- section-id: implementation -->
## 구현 가이드
> 2026-07-28 `/branch-spec` 조사(GraphQL over HTTP draft + gRPC status codes + Connect protocol)로 채웠다. 규격이 확정해 주는 것과 우리가 정해야 하는 것을 절마다 구분했다. 후자는 `UNSUPPORTED_IMPL_DECISION` + trade-off 로 표시했다(CLAUDE.md §15.5 R2).
### 1. 성공 판정 — 프로토콜마다 신호가 다른 곳에 있다
> **Trace**: D2(판정 함수 분리) ← `graphql-over-http-draft-status-errors.md#C3`, `grpc-connect-status-codes-error-model.md#C3`·`#C5` / D4(본문 판정) ← `#C1`·`#C2`·`#C3` / D7(adapter 분리) ← `#C5`·`#C6`·`#C7`
성공을 판정하는 위치가 세 프로토콜에서 전부 다르다. 이것이 판정 함수를 공유할 수 없는 이유다.
| protocol | 성공 신호 위치 | 틀리기 쉬운 구현 |
|---|---|---|
| REST | HTTP 상태 코드 | — (기준선) |
| GraphQL | **본문의 `data`·`errors` 구조**. 상태 코드는 `200` 일 수도 `294` 일 수도 있다 | `200` 만 검사 → `294` 응답을 실패로 오분류. `2xx` 만 검사 → `errors` 를 놓침 |
| gRPC-Web | **trailer 의 `grpc-status`**. HTTP 는 200 이어도 실패일 수 있다 | HTTP 상태만 검사 → 실패를 성공으로 처리 |
| Connect | **HTTP 상태 코드**(오류는 non-200) + 본문 JSON 의 `code` | gRPC-Web 과 같은 코드로 처리 → 정반대 규칙이라 반드시 틀림 |
GraphQL 행이 특히 함정이다. 규격은 data 와 errors 가 함께 있으면 `294` 를 권고하고(`#C1`), 클라이언트는 상태 코드와 무관하게 본문을 처리하라고 명시한다(`#C3`). 그래서 판정은 **상태 코드를 보지 않고** `data``errors` 의 존재로 한다.
gRPC-Web 과 Connect 를 한 adapter 로 묶지 않는다. Connect 는 trailer 를 아예 쓰지 않고(`#C6`) 오류를 non-200 으로 보내며(`#C5`), 규격 자신이 gRPC-Web 과 다른 프로토콜이라고 밝힌다(`#C7`). 이름이 비슷하다는 이유로 공유하면 한쪽이 반드시 틀린다.
`UNSUPPORTED_IMPL_DECISION` — 판정 함수를 protocol 값으로 조회하는 registry 형태로 두는 것. 규격은 판정 규칙만 정하고 우리 코드 구조를 정하지 않는다. trade-off: 조건 분기 대신 조회 표를 쓰면 새 프로토콜을 추가할 때 등록 누락이 boot 시점에 드러난다. 비용은 간접 참조가 한 겹 늘어나는 것.
### 2. GraphQL `errors` 처리 — 규격과 다르게 간다
> **Trace**: D3 ← `graphql-over-http-draft-status-errors.md#C4`·`#C6`
**규격은 field error 가 있는 부분 응답을 "successful execution" 이라고 부른다**(`#C4`). 우리는 이를 `PARTIAL_RESULT_FAILURE` 로 정규화한다. 규격 준수가 아니라 **의도적 이탈**이며, 이 문장이 노트에 남아야 다음 사람이 근거를 오해하지 않는다.
이탈하는 이유는 부분 데이터가 화면에서 정상처럼 보이기 때문이다. 목록의 절반이 비어 온 응답을 성공으로 넘기면 사용자는 "데이터가 없다" 고 읽고, 우리는 오류를 관측하지 못한다.
telemetry 에는 error 개수와 path 개수만 남기고 error message 본문은 남기지 않는다. `#C6` 대로 `data: null` 이면 errors 가 반드시 있으므로, `data` 유무만으로 request error 와 field error 를 가를 수 있다.
`UNSUPPORTED_IMPL_DECISION` — 부분 데이터를 **버리는** 것(수신은 하되 use case 에 넘기지 않음). 규격도 우리 근거도 "버려라" 라고 말하지 않는다. trade-off: 살려서 넘기면 use case 마다 "이 데이터가 완전한가" 를 판단해야 하고 그 판단이 빠지는 순간 조용한 오류가 된다. 비용은 일부 필드만 실패한 응답에서 쓸 수 있는 데이터까지 잃는 것.
### 3. gRPC 코드 매핑 — 규격이 우리에게 넘긴 결정
> **Trace**: D6 ← `grpc-connect-status-codes-error-model.md#C1`·`#C2`
코드 목록은 규격이 준다 — 0(OK)부터 16(UNAUTHENTICATED)까지 17개(`#C1`). 그러나 **재시도 가능 여부는 규격이 정하지 않는다.** 원문이 명시적이다: *"individual applications must make their own determination as to which status codes should cause an RPC to be retried"*(`#C2`).
따라서 매핑표는 `FE-REG-ERROR` 에 **우리 결정으로 등록**하고, 각 행에 왜 그렇게 정했는지를 남긴다. "규격이 그렇다" 는 근거로 쓸 수 없다. 매핑이 backend 의 코드 사용 관습과 어긋나면 재시도가 과하거나 부족해지므로, 표를 만든 뒤 backend 소유자와 대조하는 것이 `FE-GATE-030` 이전 단계로 필요하다.
`UNSUPPORTED_IMPL_DECISION` — 17개 코드를 어떤 kind 로 접을지, 그중 무엇을 retryable 로 둘지. 규격이 판단을 위임했으므로 이 표 전체가 우리 trade-off 다. 지금 값을 적지 않는 이유는 backend 대조 없이 정하면 두 번 정하게 되기 때문이다.
### 4. 이 branch 가 남기지 않는 것 (R3)
- 디코드 이후 payload 의 스키마 검증 → `DELEG-FE-010` 로 [[raw/branch-notes/feature-runtime-schema-validation-contract]] 에 위임(`FLOW-FE-RESP-004`~`006`)
- error kind 의 등록과 총함수 정규화 → [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] 소유(`FE-OC-008`). 이 branch 는 신규 3종의 **진입 조건**만 정한다
- timeout·retry·idempotency 기본값 → [[raw/branch-notes/feature-api-client-response-envelope-contract]] 소유(`FE-OC-006`)
- 스키마·코드 생성 파이프라인 SSOT → `FE-Q-013`
- 스트림 protocol(`sse`·`websocket`·`poll`) → [[raw/branch-notes/feature-frontend-realtime-subscription-lifecycle-contract]] 소유
<!-- section-id: edge-failure-dependency -->
## 엣지·실패·의존
- **실패·엣지 경로**
- HTTP 200 + `grpc-status` 비0 → `PROTOCOL_STATUS_MISMATCH`. status 는 transport 와 별도로 전달되므로(`grpc-connect-status-codes-error-model#C3`) HTTP 상태만 보는 구현은 실패를 성공으로 처리한다 (D2)
- protobuf/GraphQL 디코드 실패 → `CODEC_DECODE_FAILURE`, 본문을 telemetry 에 남기지 않음
- **GraphQL 응답에 `errors` 존재** → `PARTIAL_RESULT_FAILURE`, error 개수와 path 개수만 telemetry. 규격은 이를 "successful execution" 이라 부르므로 의도적 이탈이다(`graphql-over-http-draft-status-errors#C4`) (D3)
- **GraphQL 상태 코드가 `294`** → 실패가 아니다. data 와 errors 가 함께 있다는 규격 권고 신호이므로(`#C1`) 본문 구조로 판정한다. `200` 만 성공으로 보는 구현은 이 응답을 잘못 분류한다 (D4)
- **Connect 오류를 HTTP 200 으로 기대** → Connect 는 오류를 non-200 으로 보낸다(`#C5`). gRPC-Web 판정 코드를 그대로 쓰면 여기서 어긋난다 (D7)
- capability OFF 또는 브라우저 미지원 → REST gateway 로 fallback (`degraded-alternative`)
- gateway 도 없으면 `CAPABILITY_UNSUPPORTED`
- **다른 계약 의존**
- [[raw/branch-notes/feature-api-client-response-envelope-contract]] — timeout·retry·idempotency 기본값(`FE-OC-006`). D6 의 재시도 매핑이 이 기본값 위에 얹히므로, 상위 retry 상한과 protocol status 기반 재시도가 곱해지지 않아야 한다
- [[raw/branch-notes/feature-runtime-schema-validation-contract]] — `FLOW-FE-RESP-004`~`006`(`DELEG-FE-010`). 디코드 산출물이 이 단계로 넘어가며, 디코드가 타입을 보장한다고 건너뛰면 안 된다
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] — 신규 3종 kind(`PROTOCOL_STATUS_MISMATCH`·`CODEC_DECODE_FAILURE`·`PARTIAL_RESULT_FAILURE`)의 등록과 `defaultRetryable`. D6 의 매핑표가 이 registry 에 들어간다
- [[raw/branch-notes/feature-boundary-mapper-viewmodel-contract]] — DTO→model 매핑. 디코드 산출물이 mapper 입력이며, 프로토콜별로 산출물 모양이 달라지면 mapper 가 프로토콜을 알게 되어 D1 이 무너진다
- [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] `D4``CAP_FE_ALT_PROTOCOL` 해석 시점. flag OFF 면 protocol adapter 가 번들에 없어야 한다(`FE-GATE-033`)
<!-- section-id: claims-to-verify -->
## 검증해야 할 주장
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| `200 OK` + `errors[]` 가 success 로 반환되지 않는다 | GraphQL 클라이언트 기본 동작이 부분 성공 | negative fixture — 해당 응답 주입 후 `PARTIAL_RESULT_FAILURE` 확인 | `planned` |
| HTTP 200 + `grpc-status: 13` 이 실패로 정규화된다 | transport status 만 보는 구현이 흔함 | negative fixture — trailer 주입 후 kind 확인 | `planned` |
| protocol adapter 가 신규 port 를 만들지 않았다 | 구현 중 편의로 port 가 늘어나기 쉬움 | architecture fixture — `application/ports/` 파일 수가 늘지 않았는지 | `planned` |
| REST gateway fallback 이 실제로 도달한다 | capability OFF 경로가 테스트에서 빠지기 쉬움 | integration test — flag OFF 로 같은 operation 호출 | `planned` |
| codec 산출물이 반드시 스키마 검증을 거친다 | 디코드가 이미 타입을 보장한다고 착각하기 쉬움 | negative fixture — 스키마 위반 디코드 결과 주입 후 `SCHEMA_MISMATCH` | `planned` |
| `grpc-status` 매핑표가 backend 의 코드 사용과 일치한다 | 규격은 재시도 판정을 애플리케이션에 위임했으므로(`grpc-connect-status-codes-error-model#C2`) 대조 상대가 명세가 아니라 **backend 소유자**다 | 매핑표 초안 작성 후 backend 소유자와 코드별 의미 대조 | `needs-confirmation` |
| GraphQL 상태 코드 `294` 응답이 올바르게 처리된다 | `200` 만 검사하는 구현이 흔하고, draft 권고라 실제로 오는지도 미확인 | negative fixture — `294` + data + errors 응답 주입 후 `PARTIAL_RESULT_FAILURE` 확인. 별도로 backend 가 `294` 를 보내는지 실측 | `needs-confirmation` |
| Connect adapter 가 gRPC-Web 판정 코드를 공유하지 않는다 | 이름이 비슷해 리팩터로 합쳐지기 쉬움 | architecture fixture — 두 adapter 가 같은 성공 판정 함수를 참조하지 않는지 | `planned` |
| GraphQL 성공 판정이 상태 코드에 의존하지 않는다 | 상태 코드 검사가 습관적으로 들어감 | fixture — 같은 본문을 `200`·`294` 두 상태로 주입했을 때 판정 결과가 같은지 | `planned` |
| 부분 데이터를 버리는 정책이 제품에서 수용 가능하다 | 규격은 이를 성공이라 부르므로 이탈 비용을 제품이 감당해야 함 | 제품 소유자 확인 — 목록 절반이 실패한 응답을 통째로 버려도 되는지 | `needs-confirmation` |
## Audit & Findings
> `/branch-spec` 2026-07-28 조사에서 규격 원문과 대조해 발견한 것. hub 소유 항목은 정합 권고만 남긴다(hub §3.3).
| Finding ID | 대상 | 현재 서술 | 조사 결과 | 권고 | 처리 |
|---|---|---|---|---|---|
| `GRAPHQL_STATUS_ASSUMPTION` | hub §8.2 실패 매트릭스 및 이 노트의 `D3` 초안 | "GraphQL `200 OK` + `errors[]`" — 상태 코드를 `200` 으로 특정한다 | `graphql-over-http-draft-status-errors#C1`: data 와 errors 가 함께 있으면 규격은 **`294`** 를 권고한다. `#C3`: 클라이언트는 상태 코드와 무관하게 본문을 처리해야 한다 | 진입 조건을 "`200 OK` + `errors[]`" 가 아니라 "응답 본문에 `errors` 존재" 로 바꿀 것. 상태 코드를 조건에 넣으면 `294` 를 놓친다 | `open` — hub §8.2 정정 후보 (`compatibility_impact: none`, 진입 조건의 정확화) |
| `SPEC_DEVIATION_UNDECLARED` | 이 노트의 `D3` 초안 | "부분 성공이 아니라 `PARTIAL_RESULT_FAILURE` 로 정규화한다" — 규격을 따르는 것처럼 읽힌다 | `graphql-over-http-draft-status-errors#C4`: 규격은 field error 상황을 "partial response" 이자 **"successful execution"** 이라고 부른다 | 이탈임을 명시할 것. 이탈 자체는 유효하나 근거를 규격으로 오인하면 안 된다 | `resolved` 2026-07-28 — D3 서술과 §구현 가이드 2 에 명시 |
| `RETRY_MAPPING_MISATTRIBUTED` | 이 노트의 TODO "`grpc-status` → 정규화 kind 매핑표 (명세 확인 후)" | 명세를 읽으면 매핑이 나온다고 전제한다 | `grpc-connect-status-codes-error-model#C2`: *"individual applications must make their own determination as to which status codes should cause an RPC to be retried"* — 규격이 판단을 명시적으로 위임한다 | "명세 확인 후" 를 "우리가 정하고 backend 와 대조" 로 바꿀 것 | `resolved` 2026-07-28 — D6 신설, TODO 문구 교체 |
| `NO_GROUND_TRUTH` | `/branch-spec` §2 ca-tmpl 대조 | 명령은 ca-tmpl registry·코드와 대조하라고 요구 | `ca-tmpl` 은 Gradle/Java 백엔드 전용. frontend 구현 repo 미식별 | 전 항목 `planned` 유지 | `open``FE-Q-001` 선행 |
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
> 2026-07-28 `/branch-spec` §8b. **agent dispatch 없이 수기 판정**했다(세션 제약). governing doc = [[raw/project-notes/ca-skeleton-frontend-operational-contract]]. 기준은 hub §2.2 의 10개 universal acceptance question.
| # | 관심사 | 판정 | 근거 |
|---:|---|---|---|
| 1 | 문제와 실패 모드가 구체적인가 | covered-here | §목표 — 빈 화면이 정상처럼 보이는 경로. §엣지 7개 |
| 2 | 상속한 결정을 실제로 적용했는가 | covered-here | `PROTOCOL-001` → D1·D7, `VALIDATION-001``DELEG-FE-010`, `CAPABILITY-001``CAP_FE_ALT_PROTOCOL` 소유 |
| 3 | project-wide default 와 limit | covered-here | D2(판정 분리), D4(본문 판정), D6(매핑은 우리 결정), D7(adapter 분리) |
| 4 | 대안을 검토했는가 | covered-here | §결정 사항 — 프로토콜별 port / 부분 성공 상태 신설. D1 은 trade-off 명시 |
| 5 | 금지 구현 | covered-here | §구현 가이드 1 — 판정 함수 공유 금지, 상태 코드만 검사 금지. §구현 가이드 3 — 규격 인용으로 재시도 근거 대체 금지 |
| 6 | 실패 경로가 error kind 로 매핑되는가 | covered-here | 신규 3종 + `CAPABILITY_UNSUPPORTED`. 등록은 error-classification branch 소유 |
| 7 | 관측 가능한가 | should-fix | `PARTIAL_RESULT_FAILURE` 의 error·path 개수는 정했으나, 어느 protocol 로 처리됐는지를 구분하는 차원이 `FE-REG-TELEMETRY` 에 없다. gateway fallback 발생률도 미등록 |
| 8 | 위임 경계가 명확한가 | covered-here | `DELEG-FE-010`, §구현 가이드 4 의 R3 목록 |
| 9 | 검증 수단이 있는가 | covered-here | §검증해야 할 주장 11행, `FE-GATE-030` fixture + protocol mapping report |
| 10 | 말할 수 있는 범위 | covered-here | 전 항목 `planned``NO_GROUND_TRUTH` |
**판정: Covered (missing 0)** · Should-fix 1건(관심사 7). Blocking 아님.
## 마주친 문제
없음.
## 묶음 (이 branch에서 파생된 자료)
### Sub-branches (세부 작업)
아직 없음.
### 오류 기록 (이 branch 작업 중 발생)
아직 없음.
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
아직 없음.
### 강의 (이 작업을 위해 학습한 강의)
아직 없음.
### job-posting tie-ins (이 작업에서 파생된 글감)
아직 없음.
## 관련 일일 노트
- 아직 없음
## 완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented` 항목: 없음
- `locally-verified` 항목: 없음
- `prod-verified` 항목: 없음
- **추출하지 않을 항목**: 현재 전 항목 `planned`