/shared/codebase/clean-architecture-backend-template/src/grpc-advanced/grpc-advanced-compat/src/main/resources/envoy/envoy.yaml --- # grpc-advanced-compat 완전 해부 > 상태: COMPLETE > 재오픈 게이트: cycle 2 — `src/main` production 17파일 축자 통독 완료. `STRUCTURAL_ONLY` 잔여 없음. > 기준 revision: `21234e38cdb9a926cbc92bb97a2aee2e4a7d2916` > 분석 범위: `src/grpc-advanced/grpc-advanced-compat` > SSOT owner: `grpc-advanced-compat` > integration/family document: `analysis/20-grpc-platform.md` (secondary, INTEGRATION_ONLY) --- ## 0. SSOT identity / 커버리지 - `runtime_memberships`: **`[]`** — build-only - 다섯 다리: gRPC-Web · Servlet · Spring Integration · Reactor · Kotlin | 패키지 | 파일 | LOC | |---|---:|---:| | `web` | 4 | 220 | | `kotlin` | 3 | 164 | | `servlet` | 3 | 152 | | `reactor` | 4 | 233 | | `integration` | 3 | 193 | ### Coverage ledger | scope | count | disposition | reason | |---|---:|---|---| | `main/java/**` | 17 | `FULL_READ` | 962줄 전 본문 | | `main/resources/envoy/envoy.yaml` | 1 | `FULL_READ` | 70줄 전문. 참조 프록시 설정 — 이전 판의 ledger 에 아예 없었다 | | `test/java/**` | 5 | `FULL_READ` | 523줄 | | `build.gradle` | 1 | `FULL_READ` | 전문 | | `gradle.lockfile` | 1 | `STRUCTURAL_ONLY` | 잠금 파일 | `UNCLASSIFIED` 0. --- ## 1. 모듈의 정체와 코틀린 레인의 처리 ```groovy // build.gradle:6-10 // No Kotlin source set (adaptation D7): this repository has no Kotlin toolchain, so the Kotlin lane // is expressed as a Java-side boundary contract whose compatibility gate fails closed until a real // toolchain lane exists. Everything the gate would otherwise assert — one schema source, coroutine // cancellation propagation, Flow backpressure inside the Stable buffer limits, evidence type // preservation — is a checkable contract without it. ``` 그리고 게이트가 그 판단을 코드로 반복한다. > "Fails closed in this repository, and says so rather than reporting a pass it cannot justify… > a gate that reported success anyway would put an unverified claim in the support matrix." `blockers` 는 다섯 항목을 낸다. 넷은 프로파일에서 확인 가능하고, 다섯째가 툴체인 레인 부재다. `supportableHere()` 는 상수 거짓이다. 이 처리가 이 저장소의 다른 곳(`grpc-advanced-diagnostics` 의 인프라 테스트킷 계약)과 같은 원칙이다 — 인프라 없이 도는 묶음은 통과하고 아무것도 세우지 않는다. ## 2. 다리마다 무엇을 거절하는가 | 다리 | 거절 | |---|---| | gRPC-Web | 브라우저에 노출된 메서드 중 gRPC-Web 이 나를 수 없는 RPC 종류 | | Servlet | 컨테이너가 제공하지 않는 전송 설정 요구 | | Kotlin | 다섯 블로커(툴체인 레인 포함) | | Spring Integration | 변환기 없는 다리, 허용 목록 밖 헤더 | | Reactor | (§17.2) | gRPC-Web 의 근거: > "Two schemas — one for browsers, one for services — is how a field ends up meaning something > different depending on which client asked, and the divergence is only visible to whoever reads > both files." Servlet 의 근거: > "Refuses rather than warns, because the setting would otherwise be accepted and ignored. A > keepalive configured on a Servlet deployment does nothing, the connections behave as the container > decides, and the investigation starts from the assumption that the setting is in force." 그리고 Servlet 실행이 Netty 인증을 대신할 수 없다는 것을 상수로 못박는다. ## 3. Spring Integration 다리가 무엇을 약속하지 않는가 > "A Spring Integration `Message` accumulates headers as it moves through a flow — routing keys, > correlation ids, errors channels, whatever a transformer added — and copying them onto gRPC > metadata sends a service's internal plumbing across the network." > "The bridge does not add durability. Spring Integration channels can look like a broker, and a > bridge that implied acknowledgement or redelivery semantics would be promising something gRPC does > not do." 변환기가 없는 다리는 생성자가 거부한다 — 반사에 맡기는 것이 예상 밖 타입이 유선에 닿는 경로다. ## 12. negative-space probes **12.1 도달성.** Advanced 가족이므로 배선 경로가 없다. 그 위에 이 리프에는 테스트조차 없는 타입이 둘 있다(§17.2). **12.2 대조군 — 메타데이터 경로 둘.** `grpc-client` 의 `GrpcClientMetadataPolicy.materialize` 는 허용 목록으로 거른 뒤 `budget.check(accepted)` 를 부른다. 이 리프의 `GrpcIntegrationBridgePolicy.metadataFrom` 은 허용 목록으로 거르고 예산을 부르지 않는다(§17.1). **12.3 리프 전체의 외부 참조가 0 이다.** 재통독에서 다섯 패키지를 각각 확인했다. ``` dev.caskeleton.grpc.advanced.{web, servlet, integration, reactor, kotlin} → 리프 밖 참조 0 ``` 같은 Advanced 가족의 `grpc-advanced-bootstrap` 조차 이 리프의 타입을 하나도 부르지 않는다. Advanced 는 기능 플래그로 도달한다는 것이 이 가족의 규약인데, 그 플래그가 가리킬 대상이 배선되어 있지 않다. **12.4 드리프트.** build.gradle 이 서술한 다섯 다리와 코틀린 레인의 처리 방식이 코드와 일치한다. 다만 ledger 가 `main/resources` 를 세지 않고 있었다(§17.3 의 재료가 거기 있다). ## 16. 확인하지 못한 것 - 실제 브라우저·프록시·서블릿 컨테이너로 어떤 다리도 돌리지 않았다. 그 인프라가 필요하다는 것이 이 가족의 기록이다. - 코틀린 툴체인이 없으므로 코틀린 계약 넷을 실행으로 확인할 수 없다. ## 17. 손볼 것 ### 17.1 P3 — 통합 다리의 메타데이터 조립이 메타데이터 예산을 검사하지 않는다 ```java public Map metadataFrom(Map messageHeaders) { Map metadata = new LinkedHashMap<>(); headerAllowlist.forEach(key -> { Object value = messageHeaders.get(key.name()); if (value != null) { metadata.put(key, String.valueOf(value)); } }); return Map.copyOf(metadata); } ``` 허용 목록으로 키를 거르지만 값의 크기도, 합계도 보지 않는다. 클래스 javadoc 자신이 예산을 이 정책의 이유 중 하나로 든다 — 흐름의 내부 배관을 네트워크로 보내면 "it counts against the metadata budget." 그리고 같은 저장소의 다른 메타데이터 경로는 예산을 검사한다. ```java // GrpcClientMetadataPolicy.materialize proposed.forEach((key, value) -> { if (allowed.contains(key) && value != null) accepted.put(key, value); }); budget.check(accepted); // ← 이 줄이 이 다리에는 없다 ``` `String.valueOf(value)` 이므로 헤더 값이 임의의 객체일 때 그 문자열 표현이 그대로 실린다. Spring Integration 헤더에는 컬렉션이나 도메인 객체가 흔히 들어가므로 값 하나가 클 수 있다. 수정은 이 record 에 `GrpcMetadataBudget` 를 성분으로 추가하고 `metadataFrom` 끝에서 검사하는 것이다. 형태가 이미 옆 리프에 있다. ### 17.2 P3 — 반응형 표면 두 타입은 테스트조차 없다 ``` ReactiveGrpcClient 저장소 전체에서 등장하는 파일 1개 (자기 자신) ReactiveGrpcServerAdapter 저장소 전체에서 등장하는 파일 1개 (자기 자신) ``` 이 가족의 다른 미참조 Advanced 타입은 전부 테스트가 하나씩 있다 — 같은 패키지의 `GrpcReactorCancellationBridge` 는 2개 파일, `GrpcReactorContextBridge` 는 4개 파일에 등장한다. 두 타입은 채택자가 부를 표면이므로 production 참조 0 이 설계와 모순되지는 않는다. 어긋나는 것은 검증이다. 채택자용 표면이면 그 계약이 무엇인지를 테스트가 붙들어야 하고, 이 가족은 다른 곳에서 정확히 그렇게 한다. `ReactiveGrpcClient` 의 javadoc 이 "Exposes a unary call as a `Mono` and a server stream as a `Flux`" 라고 적는데, 그 사상이 취소와 배압에서 어떻게 동작하는지는 어디에서도 확인되지 않는다. 같은 리프의 `GrpcReactorCancellationBridge` 가 취소 전파를 다루므로 둘을 함께 검증할 자리가 이미 있다. ### 17.3 P3 — 저장소가 참조 프록시 설정을 갖고 있는데, 그것을 판정할 코드에 넣지 않는다 이 리프에는 두 가지가 함께 있다. - `GrpcWebProxyContract.violations(profile, exposedHeaders, allowedOrigins)` — 프록시 설정이 브라우저 클라이언트에게 통할지 판정하는 코드. - `src/main/resources/envoy/envoy.yaml` — 그 설정의 참조 구현. 그리고 설정 파일 자신이 그 관계를 주장한다. ```yaml # Shipped as a resource rather than as documentation prose because GrpcWebProxyContract asserts # against it: the CORS allowlist, the exposed trailer headers and the TLS termination are the three # things a browser client silently fails without, and a contract nobody checks is a contract that # drifts from whatever is actually deployed. ``` `GrpcWebProxyContract` 는 이 파일에 대해 아무것도 단언하지 않는다. 판정기는 시험에서 리터럴 집합을 받고, 참조 설정은 시험에서 문자열 포함으로만 확인된다. ```java // GrpcWebCompatibilityGateTest assertThat(GrpcWebProxyContract.violations(profile, requiredExposedHeaders(), Set.of("*"))) // ← 리터럴 .anySatisfy(v -> assertThat(v).contains("defeats the profile's allowlist")); … String envoy = resource("envoy/envoy.yaml"); assertThat(envoy) .contains("expose_headers: \"grpc-status,grpc-message") // ← 부분 문자열 .contains("exact: \"https://app.example.com\""); ``` 그래서 참조 설정이 `grpc-status` 를 노출하는지는 문자열이 확인하고, 그 노출이 **충분한지** 는 `requiredExposedHeaders()` 가 정의하는데, 둘을 잇는 코드가 없다. 필수 트레일러 목록이 늘어나면 판정기는 새 항목을 요구하고 참조 설정은 옛 문자열로 계속 통과한다. 이 리프의 다른 판정기들과 다른 점은 재료가 이미 저장소에 있다는 것이다 — grpc-testkit §17.5·grpc-server §17.1 은 스캔할 대상 자체를 만들어야 하지만, 여기서는 파일 하나를 파싱하면 된다. 수정은 시험이 `envoy.yaml` 의 `expose_headers` 와 `allow_origin`(`exact:`)을 뽑아 `GrpcWebProxyContract.violations` 에 넣고 비어 있음을 단언하는 것이다. 그러면 참조 설정과 계약이 한 곳에서 함께 움직인다. ### 확인된 설계(문제 아님) - **코틀린 게이트가 닫힌 실패를 하고 그 이유를 말하는 것** — 정당화할 수 없는 통과를 보고하지 않는다. - **코틀린 계약 넷을 툴체인 없이도 확인 가능하게 만든 것** — 나중에 필요한 것은 레인 추가이지 계약 작성이 아니다. - **브라우저와 기본 클라이언트가 한 스키마를 쓰게 한 것.** - **Servlet 이 제공하지 않는 설정을 경고가 아니라 거절로 다룬 것.** - **Servlet 실행이 Netty 인증을 대신하지 못한다고 못박은 것.** - **통합 다리가 브로커 의미론을 약속하지 않는다고 상수로 밝힌 것.** - **변환기 없는 다리를 생성자가 거부한 것.** - **다리가 생성된 스텁·서비스 API 를 대체하지 않는다고 밝힌 것.** --- ## Source anchors ``` src/grpc-advanced/grpc-advanced-compat/build.gradle:1-14 main/java/…/kotlin/GrpcKotlinCompatibilityGate.java:1-69 main/java/…/web/GrpcWebCompatibilityGate.java:1-56 main/java/…/servlet/GrpcServletStartupValidator.java:1-51 main/java/…/integration/GrpcIntegrationBridgePolicy.java:1-78 main/java/…/reactor/(ReactiveGrpcClient · ReactiveGrpcServerAdapter · GrpcReactorCancellationBridge · GrpcReactorContextBridge) main/java/…/web/(GrpcWebProfile · GrpcWebProxyContract · GrpcWebRpcSupport) main/java/…/servlet/(GrpcServletCompatibilityProfile · GrpcServletCapabilityMatrix) main/java/…/kotlin/(GrpcCoroutineContextBridge · GrpcKotlinProfile) main/java/…/integration/(GrpcIntegrationInboundGateway · GrpcIntegrationOutboundGateway) src/grpc/grpc-client/…/GrpcClientMetadataPolicy.java (예산 검사 대비) ```