Files
document-haness/docs/clean-architecture-backend-template/analysis/grpc/grpc-advanced-compat.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

12 KiB

/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. 모듈의 정체와 코틀린 레인의 처리

// 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-clientGrpcClientMetadataPolicy.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 — 통합 다리의 메타데이터 조립이 메타데이터 예산을 검사하지 않는다

public Map<GrpcMetadataKey, String> metadataFrom(Map<String, Object> messageHeaders) {
  Map<GrpcMetadataKey, String> 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."

그리고 같은 저장소의 다른 메타데이터 경로는 예산을 검사한다.

// 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 — 그 설정의 참조 구현.

그리고 설정 파일 자신이 그 관계를 주장한다.

# 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 는 이 파일에 대해 아무것도 단언하지 않는다. 판정기는 시험에서 리터럴 집합을 받고, 참조 설정은 시험에서 문자열 포함으로만 확인된다.

// 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.yamlexpose_headersallow_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 (예산 검사 대비)