# grpc-spring-boot-starter 완전 해부 > 상태: COMPLETE > 재오픈 게이트: cycle 2 — `src/main` production 4파일 468줄, 등록 파일 1개, test 1파일 267줄 축자 통독 완료. `STRUCTURAL_ONLY` 잔여 없음. > 기준 revision: `21234e38cdb9a926cbc92bb97a2aee2e4a7d2916` > 분석 범위: `src/grpc/grpc-spring-boot-starter` > SSOT owner: `grpc-spring-boot-starter` > integration/family document: `analysis/20-grpc-platform.md` (secondary, INTEGRATION_ONLY) --- ## 0. SSOT identity / 커버리지와 숫자 지도 - `runtime_memberships`: **`[]`** — build-only - 선언 의존: `api` project 7 · `implementation` project 3 + vendor 2 | 파일 | LOC | 성격 | |---|---:|---| | `GrpcPlatformStartupValidator` | 188 | 5개 검증 묶음, static 유틸 | | `GrpcPlatformProperties` | 139 | `ca-skeleton.grpc.platform.*` 결속 | | `GrpcPlatformAutoConfiguration` | 106 | 빈 9개 | | `GrpcPlatformConfigurationException` | 35 | 위반 목록 예외 | | **main java 합계** | **468** | | | `AutoConfiguration.imports` | 1 | 자동 설정 1개 등록 | | `GrpcPlatformStartupValidatorTest` | 267 | 테스트 | | `build.gradle` | 24 | 의존 선언 | ### Coverage ledger | scope | count | disposition | reason | |---|---:|---|---| | `main/java/**` | 4 | `FULL_READ` | 188+139+106+35 전 본문 | | `main/resources/META-INF/spring/*.imports` | 1 | `FULL_READ` | 1줄 | | `test/java/**` | 1 | `FULL_READ` | 267줄 · 테스트 12개 | | `build.gradle` | 1 | `FULL_READ` | 24줄 | | `gradle.lockfile` | 1 | `STRUCTURAL_ONLY` | 잠금 파일 | `UNCLASSIFIED` 0. --- ## 1. 모듈의 정체와 격리 규칙 ```groovy // build.gradle:3-7 // The platform's composition boundary: typed properties, auto-configuration and the startup // validator that refuses a deployment whose configuration contradicts a Stable invariant. // // It must never reach `:grpc-advanced:*`. That is not a comment — the registry's // allowed_dependencies for this leaf omits every advanced id, `verifyCleanArchitectureDependencies` // enforces it, and GrpcPlatformStartupValidatorTest asserts the same rule from the Java side. ``` 격리 규칙은 세 겹이다 — 레지스트리, 빌드 검증 태스크, 그리고 자바 쪽 단언. 세 번째는 `validateAdvancedIsolation` 이 `GrpcStableBuildInvariant.requireNoAdvancedDependency` 를 부르는 형태다. ## 2. 자동 설정이 만드는 것 `@ConditionalOnProperty(prefix = "ca-skeleton.grpc.platform", name = "enabled", havingValue = "true", matchIfMissing = false)` — 기본 꺼짐. | 빈 | 만들어지는 값 | |---|---| | `GrpcExecutorProfile` | `boundedPool(maxPoolSize, queueCapacity)` | | `GrpcServerProfile` | `stableNetty(executorProfile)` | | `GrpcAdmissionController` | `forExecutor(executorProfile)` | | `GrpcServiceHealthRegistry` | `new …(GrpcHealthPolicy.standalone())` | | `GrpcReflectionPolicy` | 설정 값이 없으면 `defaultFor(environment)` | | `GrpcAdminExposurePolicy` | `standard()` | | `GrpcDrainPolicy` | `stable()` | | `GrpcContextBinder` | `new …(GrpcContextPropagationPolicy.stable())` | | `GrpcErrorMapper` | `new …("grpc-platform", UUID::randomUUID)` | 전부 정책·프로파일·레지스트리다. 서버도, 인터셉터 사슬도, 서비스 어댑터 등록도 없다. ## 3. 설정 표면 `@ConfigurationProperties(prefix = "ca-skeleton.grpc.platform", ignoreUnknownFields = false)`. 두 판단이 javadoc 에 적혀 있다. > "Off by default, like every other optional capability in this repository. A platform that starts > because its jar is on the classpath is a platform that opens a port on a deployment nobody decided > to give one to." > "`ignoreUnknownFields = false` so a misspelled key fails startup rather than silently leaving a > setting at its default." ## 4. 검증기가 담은 규칙 javadoc 이 선정 기준을 적는다. > "Every rule here is a mistake whose runtime symptom is either silence or a misattributed failure… > None of them fails a smoke test." 다섯 묶음이다. - 전송·보안 — production 전송이 아니면 거부, 배포 환경에서 TLS 미사용·trust-all·반사 전체 공개 거부 - 실행기 — 큐 용량 1 미만(무제한) 거부, 풀 크기 양수 요구 - 메서드 — 단항인데 사용 가능한 마감이 0, 명시적 재시도가 멱등 프로파일과 모순, 멱등 키 필수인데 원장 비활성, Stable 범위 밖 RPC 종류 - 채널 — Stable 스킴 요구, 두 재시도 소유자가 동시에 in-process 재시도 - 고급 격리 — Stable 스타터가 advanced 의존을 끌면 위반 그리고 한 번에 전부 모아 실패한다 — "so a deployment learns the whole list in one restart." ## 10. 테스트 레인 `GrpcPlatformStartupValidatorTest` 267줄 · 12개. 검증기의 규칙별 거부와 통과를 직접 호출로 확인한다. **자동 설정 컨텍스트를 세우는 테스트는 없다** — `ApplicationContextRunner` 도, 슬라이스 테스트도 없다. 빈 아홉 개가 실제로 조립되는지는 이 레인이 답하지 않는다. `aCoherentConfigurationStarts` 가 통과 쪽을, 나머지 아홉이 규칙별 거부 쪽을 잡는다 — in-process 전송, TLS 둘, 반사, 무제한 실행기, 원장 없는 멱등 키, client-streaming, 재시도 소유자 둘, advanced 누출. `aRefusalNamesEveryViolation` 이 세 개를 동시에 깨뜨려 목록이 한 번에 나오는지 본다. 마지막 하나가 형태로 특이하다. ```java void theAutoConfigurationIsRegisteredAndStable() { assertThat(read(Path.of("src/main/resources/META-INF/spring/…imports")).strip()) .isEqualTo("dev.caskeleton.grpc.boot.GrpcPlatformAutoConfiguration"); // The dependency declaration, not the word: the build file's own comment says it must never // reach an advanced module, and matching on the prose would fail on the sentence stating the rule. assertThat(read(Path.of("build.gradle"))).doesNotContain("project(':grpc-advanced"); } ``` 단위 테스트가 자기 모듈의 `build.gradle` 을 파일로 읽어 의존 선언을 단언한다. 주석이 왜 낱말이 아니라 선언 문법에 맞추는지까지 적어 두었다 — 규칙을 서술한 문장 자체가 낱말 검색에 걸리기 때문이다. `verifyCleanArchitectureDependencies` 가 도는 것과 별개로 이 레인 안에서도 격리가 붙들린다. ## 12. negative-space probes **12.1 도달성.** 리프 밖에서 이 리프의 타입을 부르는 코드가 0 이고, **이 리프를 의존하는 모듈도 0 이다.** ``` $ grep -rn "grpc-spring-boot-starter" --include=*.gradle src/ (매치 없음) $ grep -rn "ca-skeleton.grpc.platform" --include=*.yml --include=*.yaml --include=*.properties . (매치 없음 — 이 리프 자신을 빼고) $ grep -rn "GrpcPlatformAutoConfiguration\|GrpcPlatformProperties\|GrpcPlatformStartupValidator" --include=*.java src/ | grep -v grpc-spring-boot-starter (매치 없음) ``` | 타입 | production 호출자 | |---|---| | `GrpcPlatformAutoConfiguration` | 등록 파일 1줄 — 그러나 이 스타터를 클래스패스에 올리는 모듈이 없다 | | `GrpcPlatformStartupValidator` | **0** | | `GrpcPlatformConfigurationException` | 검증기 안에서만 | `enabled=true` 를 쓰는 설정 파일도 저장소에 없다. 즉 `@ConditionalOnProperty` 가 참이 되는 배포가 지금 하나도 없고, 아홉 빈은 아직 한 번도 만들어진 적이 없다. §17.1 의 등급을 P2 로 둔 근거가 이것이다 — 오늘의 사고가 아니라, 이 스타터를 처음 채택하는 배포가 맞을 상태다. **12.3 선언만 있고 쓰이지 않는 의존 셋.** ```groovy implementation project(':grpc:grpc-proto-contract') implementation project(':grpc:grpc-codegen') implementation project(':grpc:grpc-operation-ledger-jpa') ``` 이 리프의 자바 4파일 어디에도 `dev.caskeleton.grpc.contract` · `…grpc.codegen` · 운영 원장 타입의 import 가 없다. `operation-ledger-enabled` 는 `boolean` 프로퍼티일 뿐 원장 타입을 참조하지 않는다. 세 의존 모두 build-only 판정 도구다 — 스키마 규칙 엔진, 코드 생성 거버넌스, JPA 원장. 스타터가 그것들을 **런타임 조립에 쓰지 않으면서 클래스패스에 끌고 온다.** 이 리프의 존재 이유가 "구성 경계"이므로, 경계가 끌어오는 것이 실제로 필요한 것인지가 다른 리프보다 더 중요하다. 같은 사실을 반대편에서도 기록해 두었다 — `grpc-codegen` §12.1, `grpc-proto-contract` §12.1. **12.2 설정 키별 소비자.** | 키 | 읽는 곳 | |---|---| | `enabled` | `@ConditionalOnProperty` | | `executor-queue-capacity` · `executor-max-pool-size` | 자동 설정 + 검증기 | | `environment` | 자동 설정(반사 정책) + 검증기 | | `reflection-mode` | 자동 설정 + 검증기 | | `transport` | **검증기뿐** | | `tls-enabled` · `trust-all-certificates` | **검증기뿐** | | `operation-ledger-enabled` | **검증기뿐** | | `default-unary-deadline` | **없음** | **12.4 드리프트.** build.gradle 이 서술한 세 요소(타입 있는 설정·자동 설정·시작 검증기)가 전부 존재한다. 어긋난 것은 세 번째가 시작 시 돌지 않는다는 점이고 §17.1 이다. ## 16. 확인하지 못한 것 - 스타터를 실제 애플리케이션에 올려 컨텍스트를 세우지 않았다. build-only 이고, 이 스타터를 의존하는 모듈이 저장소에 없다(§12.1). - `verifyCleanArchitectureDependencies` 태스크를 이 리비전에서 실행하지 않았다. - 테스트를 실행하지 않았다. 12개 전부 본문으로만 확인했다. - 세 `implementation` 의존이 쓰이지 않는다는 것(§12.3)은 패키지 이름 grep 으로 판정했다. 상수나 문자열을 통한 간접 사용이라면 잡히지 않는다. ## 17. 손볼 것 ### 17.1 P2 — 시작 검증기가 시작 시 실행되지 않는다 `GrpcPlatformStartupValidator` 를 이름으로 부르는 파일은 둘뿐이다 — 자기 자신과 자기 테스트. ``` src/grpc/grpc-spring-boot-starter/src/main/java/…/GrpcPlatformStartupValidator.java src/grpc/grpc-spring-boot-starter/src/test/java/…/GrpcPlatformStartupValidatorTest.java ``` `GrpcPlatformAutoConfiguration` 은 빈 9개를 만들고 `requireValid` 를 부르지 않는다. 초기화 콜백도, `@PostConstruct` 도, `ApplicationRunner` 도 없다. 그래서 클래스 javadoc 이 약속한 성질이 성립하지 않는다 — "Refuses to start on a configuration that would be wrong in a way nobody would notice." 지금은 그 설정으로 그냥 시작한다. **함께 사라지는 것.** 검증기가 유일한 소비자인 설정 키가 넷이다. - `transport` — production 이 아닌 전송을 거부할 곳이 없다. 게다가 자동 설정은 이 값을 보지 않고 `GrpcServerProfile.stableNetty(...)` 를 하드코딩한다(§17.2). - `tls-enabled` · `trust-all-certificates` — 배포 환경의 TLS 바닥을 강제할 곳이 없다. - `operation-ledger-enabled` — 멱등 키 필수 메서드가 원장 없이 열리는 것을 막을 곳이 없다. 같은 저장소가 이 형태를 두 번 기록했다 — `WebPlatformStartupValidator` 가 시작 시 실행되지 않고, `BrokerAclManifest` 의 시작 자기점검이 없다. 반대로 messaging 의 `StartupProfileValidation` 은 `InitializingBean.afterPropertiesSet` 으로 돌려 그 문제를 이미 한 번 해결했고, fileserver 는 `attestMapping()` 을 app-bootstrap 의 `@Bean` 으로 연결했다. 정본이 저장소 안에 둘 있다. **왜 배선되지 않았는지가 서명에 보인다.** `violations` 는 넷을 받는다. ```java public static List violations( GrpcPlatformProperties properties, // 자동 설정이 @EnableConfigurationProperties 로 가진다 GrpcMethodPolicyCatalog catalog, // 이 자동 설정에 빈 정의 없음 List channelProfiles, // 빈 정의 없음 Set stableModuleDependencies) // 이것을 런타임에 계산하는 코드가 저장소에 없음 ``` 넷 중 셋에 생산자가 없다. 특히 마지막은 "스타터가 해석한 모듈 id 집합" 인데, 그것을 실행 중에 산출하는 코드가 저장소 어디에도 없다 — 테스트는 `Set.of("grpc-core-api", "grpc-policy", "grpc-server", "grpc-client")` 리터럴을 넣는다. 검증기가 요구하는 입력을 구성 경계가 만들지 않으므로, 지금 형태로는 부를 수가 없다. **수정.** 자동 설정에 검증기를 부르는 `InitializingBean`(또는 `SmartInitializingSingleton`) 빈을 하나 추가하되, 세 입력의 생산자를 함께 정한다. - `GrpcMethodPolicyCatalog` · `List` — `ObjectProvider` 로 받고 비어 있을 때의 동작(건너뛸지, 그 자체를 위반으로 볼지)을 정한다. - `stableModuleDependencies` — 런타임에 계산할 방법이 없다면 `GrpcStableModuleCatalog` 가 아는 정적 목록으로 대체하거나, 이 규칙을 빌드 태스크 쪽에만 남기고 검증기 서명에서 뺀다. 지금은 같은 불변식을 세 겹으로 둔다고 §1 이 말하지만, 세 번째 겹이 실행되려면 아무도 만들지 않는 입력이 필요하다. ### 17.2 P3 — 자동 설정이 `transport` 를 읽지 않고 전송을 하드코딩한다 ```java @Bean @ConditionalOnMissingBean public GrpcServerProfile grpcServerProfile(GrpcExecutorProfile executorProfile) { return GrpcServerProfile.stableNetty(executorProfile); } ``` `GrpcPlatformProperties.transport` 는 `GrpcServerTransport` 열거형이고 기본값이 `NETTY_SHADED` 다. 그 값을 자동 설정이 보지 않으므로 다른 값을 설정해도 만들어지는 프로파일은 같다. 지금은 무해에 가깝다 — 기본값이 하드코딩된 것과 같고, 다른 값은 §17.1 때문에 거부되지도 않지만 반영되지도 않는다. 그러나 설정 키가 존재하고 문서화되어 있으므로 운영자는 그것이 전송을 고른다고 읽는다. 수정은 프로파일 팩토리를 `transport` 로 분기시키거나, 그 키를 검증 전용임을 자바독에 명시하는 것이다. ### 17.3 P3 — `default-unary-deadline` 은 읽는 코드가 저장소에 없다 ```java /** The default deadline applied to a Stable unary method that declares none. */ private Duration defaultUnaryDeadline = Duration.ofSeconds(2); ``` `getDefaultUnaryDeadline()` 의 호출자가 0 이다. 검증기도 이 값을 쓰지 않는다 — 검증기가 보는 것은 정책 목록의 `policy.deadline().usable()` 이고 그 값이 0 이면 위반을 낸다. 즉 자바독이 말하는 "선언하지 않은 메서드에 적용되는 기본 마감" 을 적용하는 코드가 없다. `ignoreUnknownFields = false` 라서 이 키를 설정하는 것은 성공하고 아무 효과가 없다. 수정은 그 기본값을 실제로 적용하는 지점을 만들거나(정책 목록 조립 시), 필드를 제거하는 것이다. ### 17.4 P3 — 반사 모드를 명시하면 서비스·역할 허용 목록이 조용히 하드코딩으로 바뀐다 ```java @Bean @ConditionalOnMissingBean public GrpcReflectionPolicy grpcReflectionPolicy(GrpcPlatformProperties properties) { return properties.getReflectionMode() == null ? GrpcReflectionPolicy.defaultFor(properties.getEnvironment()) : new GrpcReflectionPolicy( properties.getReflectionMode(), java.util.Set.of("admin"), // ← 리터럴 java.util.Set.of("ROLE_PLATFORM_ADMIN")); // ← 리터럴 } ``` 두 갈래가 만드는 것이 같은 종류의 값이 아니다. - 설정하지 않으면 `defaultFor(environment)` — 환경이 서비스 목록과 역할 목록을 함께 결정한다. - 설정하면 모드만 운영자 것이고, **허용 서비스와 허용 역할은 이 자동 설정에 박힌 리터럴이 된다.** 운영자가 조정한다고 생각하는 것은 노출 수위 하나인데, 실제로는 노출 대상 집합까지 바뀐다. 그리고 그 두 리터럴은 설정 표면에 노출되어 있지 않으므로 되돌릴 방법이 `reflection-mode` 를 다시 비우는 것뿐이다. `ca-skeleton.grpc.platform` 은 `ignoreUnknownFields = false` 를 걸어 "오타가 조용히 기본값으로 남지 않게" 한 설정 표면이다. 같은 규율로 보면, 값을 하나 설정했을 때 설정하지 않은 두 값이 함께 바뀌는 것도 같은 종류의 침묵이다. **수정.** 허용 서비스·역할을 `GrpcPlatformProperties` 에 올리거나, 명시 모드에서도 `defaultFor(environment)` 가 만든 정책의 모드만 바꾼 사본을 쓴다. 후자가 이 저장소의 다른 곳에서 쓰는 형태다(`GrpcProtoStyleManifest.allowingWellKnownTypes` 처럼 넓힌 사본). ### 확인된 설계(문제 아님) - **기본 꺼짐과 그 근거** — 클래스패스에 있다는 이유로 포트를 여는 플랫폼이 되지 않는다. - **`ignoreUnknownFields = false`** — 오타가 조용히 기본값으로 남지 않는다. - **고급 격리를 세 겹으로 둔 것** — 레지스트리·빌드 태스크·자바 단언. - **검증기가 한 번에 전부 보고하는 것** — 재시작 한 번으로 목록 전체를 배운다. - **검증 규칙 선정 기준** — 증상이 침묵이거나 오귀인인 실수만 담는다. - **`@ConditionalOnMissingBean` 을 아홉 빈 전부에 둔 것** — 채택자가 개별 정책을 갈아끼울 수 있다. --- ## Source anchors ``` src/grpc/grpc-spring-boot-starter/build.gradle:1-24 main/java/…/boot/GrpcPlatformAutoConfiguration.java:1-106 main/java/…/boot/GrpcPlatformStartupValidator.java:1-188 main/java/…/boot/GrpcPlatformProperties.java:1-139 main/java/…/boot/GrpcPlatformConfigurationException.java:1-35 main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:1 test/java/…/boot/GrpcPlatformStartupValidatorTest.java:1-267 ```