# 타입 안전 gRPC 실행 플랫폼 — 이 저장소 규약으로의 어댑팅 설계 두 계획 문서를 이 저장소(`ca-skeleton`)의 실제 구조·정책·빌드 게이트에 맞춰 실행 가능한 형태로 옮기는 설계다. - 원본 A: `docs/2026-08-13-grpc-type-safe-rpc-platform-implementation-plan.md` (Stable, Task 1–53) - 원본 B: `docs/2026-08-13-grpc-advanced-capabilities-expansion-plan.md` (Advanced, Task 1–18) 원본은 `modules/grpc/**` · Gradle Kotlin DSL · 패키지 `io.backend.skeleton.grpc` · Spring Boot 4.1을 전제한다. 이 저장소는 `src/**` · Groovy DSL · `dev.caskeleton` · Spring Boot 4.0.8 · fail-closed module registry를 쓴다. 원본 Global Constraint의 마지막 항목이 이 어댑팅을 명시적으로 허용한다: "실제 저장소 구조가 예상 경로와 다르면 파일 경로만 매핑하고 공개 계약·불변 조건·테스트 의미는 변경하지 않는다." ## 1. 패밀리 위치 — 왜 `adapter:inbound:grpc` 확장이 아닌가 원본이 기술하는 것은 인바운드 어댑터 하나가 아니라 **자기 API·SPI·adapter·조립 경계를 가진 벤더드 RPC 플랫폼**이다. 이 저장소에는 그 형태의 선례가 이미 있다 — `messaging:*`. root `CLAUDE.md`가 직접 그렇게 규정한다: "vendored messaging platform: a product with its own API, SPI, adapters and composition boundary, not a layer of this application". 따라서 gRPC 플랫폼도 같은 자리에 둔다. ```text src/grpc/ → :grpc:* Stable 플랫폼 (12 leaf) src/grpc-advanced/ → :grpc-advanced:* Advanced/Experimental (6 leaf) src/adapter/inbound/grpc 기존 인바운드 전송 어댑터 (그대로 유지) ``` `grpc-advanced`를 별도 디렉터리·별도 Gradle prefix로 分離하는 이유는 원본 Stable Task 1의 불변 조건 "Stable starter가 `modules/grpc-advanced`를 참조하면 build를 실패시킨다"를 registry의 `allowed_dependencies`만으로 기계 검증할 수 있게 만들기 위해서다. `verifyCleanArchitectureDependencies`가 그 게이트다. 패키지 루트는 `dev.caskeleton.grpc` / `dev.caskeleton.grpc.advanced`다. ## 2. Leaf 매핑 — 원본 31개 모듈 → 이 저장소 18개 leaf 원본 모듈 경계 중 **이 저장소가 이미 다른 메커니즘으로 표현하는 것**만 합친다. 능력(capability)은 하나도 버리지 않는다. ### Stable — `src/grpc/` | leaf | 원본 모듈 | 담는 Task | 의존 | | --- | --- | --- | --- | | `grpc-core-api` | grpc-core-api | 2–7 | (없음, Java stdlib) | | `grpc-proto-contract` | grpc-proto-contract | 8 | core-api | | `grpc-codegen` | grpc-codegen | 9–11 | core-api, proto-contract | | `grpc-policy` | grpc-policy | 12, 16, 17, 20, 28–31, 33, 34, 37–43 | core-api | | `grpc-server` | grpc-server | 13–15, 18, 19 | core-api, policy | | `grpc-client` | grpc-client | 24–27 | core-api, policy | | `grpc-discovery` | grpc-discovery | 35, 36 | core-api, client | | `grpc-admin` | grpc-admin | 21–23, 45 | core-api, server | | `grpc-observability` | grpc-observability | 44 | core-api | | `grpc-operation-ledger-jpa` | grpc-operation-ledger-jpa | 32 | core-api | | `grpc-spring-boot-starter` | grpc-spring-boot-starter | 52 | 위 전부 | | `grpc-testkit` | testkit-core + -inprocess + -netty + -fault | 46–51, 53 | 위 전부 | **testkit 4개를 1개로 합친 근거.** 원본이 testkit을 넷으로 쪼갠 목적은 "in-process 증거를 실제 네트워크 증거로 오인하지 않게 한다"이다. 이 저장소는 그 목적을 모듈 경계가 아니라 **strict test lane** 컨벤션(`ca.strict-test-lane`)으로 이미 표현한다 — lane은 태그/소스셋/명시 테스트 중 하나로만 선택하고, 아무것도 실행하지 않으면 실패하며, up-to-date 결과를 제공하지 않는다. 그래서 네 모듈은 `grpc-testkit` 한 leaf 안의 네 lane이 된다: `grpcInProcessContractTest`, `grpcNettyContractTest`, `grpcFaultTest`, `grpcPerformanceTest`. 원본의 "in-process는 HTTP/2·TLS 증거가 아니다"는 lane 분리와 `GrpcEvidenceGrade`로 강제한다. ### Advanced — `src/grpc-advanced/` | leaf | 원본 모듈 | 담는 Task | | --- | --- | --- | | `grpc-advanced-bootstrap` | grpc-advanced-bootstrap | A1, A18 | | `grpc-advanced-edition` | grpc-edition-2024 + grpc-edition-2026-experimental | A2, A3 | | `grpc-advanced-streaming` | grpc-client-streaming + grpc-bidi-streaming + grpc-manual-flow-control | A4–A7 | | `grpc-advanced-resilience` | grpc-hedging + grpc-custom-resolver + grpc-custom-load-balancer + grpc-xds | A8–A11 | | `grpc-advanced-compat` | grpc-web + grpc-servlet-compat + grpc-integration-bridge + grpc-reactor + grpc-kotlin | A12–A16 | | `grpc-advanced-diagnostics` | grpc-channel-diagnostics | A17 | Advanced 모듈은 capability마다 leaf를 나누는 대신 **capability grade와 feature flag** (`GrpcAdvancedCapability` / `GrpcAdvancedFeatureFlags`)로 분리한다. A18의 요구사항 "Edition, streaming, xDS, gRPC-Web, Servlet, language adapter가 서로의 승격을 묶지 않는다"는 leaf 경계가 아니라 capability별 독립 promotion evidence로 표현되므로, 합쳐도 그 불변 조건은 유지된다. ## 3. 원본과 달라지는 지점 (deviation)과 근거 | # | 원본 | 이 저장소 | 근거 | | --- | --- | --- | --- | | D1 | Spring Boot 4.1 BOM | Spring Boot 4.0.8 BOM | 저장소 실제 baseline(`src/build.gradle:13`). BOM이 SSOT라는 계약 자체는 유지 | | D2 | Boot-managed Spring gRPC starter | self-managed `io.grpc` (`ext.grpcVersion`) | 기존 기록된 결정(`adapter/inbound/grpc/README.md` "왜 self-managed Netty 인가"). starter 커플링 회피 | | D3 | Gradle Kotlin DSL, `settings.gradle.kts` | Groovy DSL + `config/architecture/modules.json` | registry가 leaf 목록의 SSOT이고 settings는 그것을 읽기만 한다 | | D4 | `io.backend.skeleton.grpc` | `dev.caskeleton.grpc` | 저장소 기본 패키지 | | D5 | Buf CLI (`bufLint`/`bufBreaking` 등) | 저장소 소유 규칙 엔진 + Gradle verify task | Buf CLI 바이너리가 이 환경에 없다. lint/format/breaking **규칙**을 Java로 구현해 동일 판정을 내리고, CLI는 동일 규칙을 재확인하는 선택 경로로 남긴다 | | D6 | protoc/grpc-java codegen을 빌드에서 실행 | `.proto` 소스 + codegen **정책·descriptor 계약**만 실행, protoc 실행은 명시적 확장점 | 아래 별도 절 | | D7 | `grpc-kotlin`의 `.kt` 소스 | Java 쪽 coroutine/Flow **경계 계약**만 | 저장소에 Kotlin 플러그인·소스셋이 없다. A16의 5개 요구사항 중 4개(스키마 단일 소스, cancellation 전파 계약, backpressure 우회 금지, evidence 타입 보존)는 Java 계약으로 표현 가능하고, Kotlin 툴체인 lane은 `GrpcKotlinCompatibilityGate`가 미충족으로 fail-closed 판정한다 | | D8 | 새 leaf가 곧 런타임 | 신규 leaf 전부 `runtime_memberships: []` (build-only) | `messaging:*`가 처음 착지한 방식과 동일. 런타임 투입은 registry membership 변경 + `verifyRuntimeModuleMembership` 통과가 선행 조건이며, 그것은 별도 결정이다 | ### D6 — protoc 실행을 지금 켜지 않는 이유 이 저장소의 모든 leaf는 예외 없이 spotless(google-java-format) · checkstyle · spotbugs(HIGH) · errorprone · `-Werror`를 통과해야 한다(`src/build.gradle`의 `configure(subprojects)` 블록). protoc가 만든 소스는 그 어느 것도 통과하지 못하므로, 실제 codegen을 켜려면 해당 소스셋에서 다섯 게이트를 모두 끄는 carve-out이 필요하다. 저장소에 선례는 있다(`jmh` 소스셋). 하지만 그 carve-out은 Task 10 하나를 위해 품질 게이트를 여는 결정이고, 그 결정은 이 작업 범위 밖의 승인 사항이다. 그래서 Task 8–11의 **불변 조건**은 전부 실행 가능한 형태로 구현한다: proto style 규칙 검증, 삭제 필드 `reserved` 이력 대조, Buf breaking category(`FILE`) 판정, generated package와 hand-written package 겹침 금지, descriptor+schema hash 릴리스 아티팩트, consumer fixture 실패 시 릴리스 차단. 빠지는 것은 protoc 프로세스 호출 하나이고, `GrpcCodegenManifest`가 그 지점을 단일 owner로 고정한 채 비워둔다. ## 4. 유지되는 원본 불변 조건 (변경 없음) - 실행 증거 3축(Transport/Business/Stream) 분리, `RESPONSE_HEADERS_SEEN` → `COMMIT_CONFIRMED` 자동 승격 금지 - `DEADLINE_EXCEEDED` mutation = `COMPLETION_UNKNOWN` 후보, `UNAVAILABLE`만으로 상태 변경 RPC 재호출 금지 - `NON_IDEMPOTENT`에 explicit retry·hedging 금지, explicit retry owner는 하나 - Stable Unary는 positive deadline 필수 - Server Streaming: bounded queue + single serialized writer, partial delivery 후 whole-call retry 금지 - Stable RPC 유형은 Unary·Server Streaming, Client/Bidi는 Advanced - production reflection 기본 비활성, dev·stage·prod TLS 필수, trust-all 금지 - Stable resolver = Static·DNS, Stable LB = pick_first·round_robin - metric tag에 raw metadata/payload/actor/tenant/object/stream/idempotency ID 금지 - Netty가 Stable certification transport, in-process는 network/TLS 증거가 아님 ## 5. 애플리케이션이 이 패밀리에 도달하는 경로 `messaging:*`의 MSG-015(bridge 부재)를 반복하지 않는다. 계약상의 경로는 ```text application-owned port → adapter:inbound:grpc (typed service adapter) → :grpc:grpc-server SPI ``` 이고, Stable Task 13/14가 그 경계를 소유한다(`GrpcApplicationBoundaryRules`, `GrpcRawApiImportRule`, `GrpcServiceAdapter`). 플랫폼이 green이 된 뒤 마지막 단계에서 `adapter-inbound-grpc`의 `allowed_dependencies`에 `grpc-core-api`·`grpc-server`를 추가하고 브리지를 배선한다. 그 전까지 플랫폼은 self-contained build-only다. ## 6. 원본 파일 목록 대비 대조 (2026-08-31 감사) 두 계획이 `Files:` 절에 명시한 산출물 전체를 기계적으로 대조했다. 감사 스크립트는 각 Task의 파일 basename이 저장소에 존재하는지 확인한다(build 산출물 제외, 단 annotation processor가 생성하는 `spring-configuration-metadata.json`은 build 출력에서 확인). ```text STABLE (Task 1–53) : present=250 missing=11 ADVANCED (Task 1–18) : present= 91 missing= 2 TOTAL : present=341 missing=13 ``` 잔여 13건은 전부 아래 편차로 설명된다. **행동이 빠진 것은 없다.** ### 6.1 ADR 파일명 (6건) — 저장소 명명 규약 계획은 `ADR-060` ~ `ADR-065` 연번을 쓴다. 이 저장소의 `docs/adr/`는 접두사 규약을 쓴다 (`ADR-WS-001`, `ADR-MONGO-004`, `ADR-WEB-ADV-003`). 내용은 1:1이다. | 계획 | 이 저장소 | | --- | --- | | ADR-060 platform-boundary | `ADR-GRPC-001-platform-family-and-registry-shape.md` | | ADR-061 execution-evidence | `ADR-GRPC-003-three-axis-execution-evidence.md` | | ADR-062 retry-idempotency | `ADR-GRPC-004-retry-ownership-and-durable-idempotency.md` | | ADR-063 streaming-resume | `ADR-GRPC-005-server-streaming-single-writer-and-resume.md` | | ADR-064 discovery-kubernetes | `ADR-GRPC-006-stable-discovery-and-kubernetes-routing.md` | | ADR-065 advanced-promotion | `ADR-GRPC-ADV-001-capability-promotion-is-per-capability.md` | 계획에 없던 `ADR-GRPC-002-schema-governance-without-protoc.md`가 추가로 있다 — D6 결정을 기록한다. ### 6.2 codegen convention plugin 3건 — D6 `io.backend.grpc-buf-conventions.gradle.kts`, `io.backend.grpc-codegen-conventions.gradle.kts`, 그 `.properties`. protoc를 실행하지 않으므로 실행할 convention plugin이 없다. 이들이 고정했을 결정은 `GrpcCodegenManifest`·`GrpcCodegenOutput`·`GrpcGeneratedPackagePolicy`가 Java로 강제하고, `buf.yaml`·`buf.gen.yaml`·`buf.lock`이 CLI가 있는 환경에서 같은 판정을 내리도록 커밋되어 있다. ### 6.3 TLS 인증서 3건 — 런타임 생성으로 대체 `ca.crt`, `server.crt`, `client.crt`. 커밋된 인증서는 만료되고, 커밋된 개인키는 개인키다. `GrpcTlsTestMaterial`이 JDK `keytool`로 fixture마다 PKCS12를 생성하고 `close()`가 지운다. Netty의 `SelfSignedCertificate`는 JDK 21에서 `sun.security.x509` 미export로 실패하므로 쓰지 않았다. ### 6.4 Kotlin 소스 1건 — D7 `GrpcCoroutineAdapter.kt`. 저장소에 Kotlin 툴체인이 없다. 계약 요구 4건은 Java로 검증하고 (`GrpcKotlinProfile`, `GrpcCoroutineContextBridge`), compile lane은 `GrpcKotlinCompatibilityGate.supportableHere()`가 `false`로 fail-closed다. ### 6.5 2026-08-31 감사에서 실제로 메꾼 것 (6건) 감사가 아니었으면 남았을 것들이다. | 항목 | 조치 | | --- | --- | | `GrpcKubernetesProfileValidator` | 이름 있는 타입으로 분리. VIP+장기스트림, drain grace < reconnect budget 두 규칙 추가 | | ADR-064 상당 (discovery/Kubernetes) | `ADR-GRPC-006` 작성 | | xDS `bootstrap.json` | 테스트 리소스로 추가 + `GrpcXdsStartupGuard.bootstrapMismatches`가 실제로 대조 (namespace 불일치·비TLS control plane 검출) | | `control-plane-snapshot.json` | 테스트 리소스로 추가 + redactor가 실제 모양의 데이터로 검증됨 | | `DocumentClientFixture.java` + fixture `build.gradle.kts` | 추가. `GrpcConsumerFixture.fromJavaSource`가 fixture 소스에서 service·method·package 요구사항을 **역산**하므로 손으로 적은 목록이 아니다 | | `buf.gen.yaml`, `buf.lock` | 추가 + proto contract 테스트가 내용을 검증 | ### 6.6 테스트 클래스 17건 — 감사 후 계획대로 분리 감사 시점에 leaf별로 통합돼 있던 테스트를 계획이 명시한 이름으로 분리했다. 분리 전에도 모든 타입이 실제로 검증되고 있었으나(통합 클래스가 해당 타입을 참조), 계획과의 추적성을 위해 나눴다: `GrpcNettyVariantSelectorTest`, `GrpcReflectionPolicyTest`, `GrpcClientMetadataPolicyTest`, `GrpcKubernetesProfileTest`, `GrpcEdition2026GuardTest`, `GrpcClientStreamPolicyTest`, `GrpcClientMessageDeduplicatorTest`, `GrpcBidiSequenceTrackerTest`, `GrpcDemandControllerTest`, `GrpcHedgingEligibilityTest`, `GrpcResolverSafetyPolicyTest`, `GrpcLoadBalancerSafetyPolicyTest`, `GrpcXdsStartupGuardTest`, `GrpcWebCompatibilityGateTest`, `GrpcServletStartupValidatorTest`, `GrpcIntegrationBridgePolicyTest`, `GrpcReactorContextBridgeTest`, `GrpcKotlinCompatibilityGateTest`.