Files
document-haness/docs/clean-architecture-backend-template/analysis/grpc/grpc-spring-boot-starter.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

296 lines
18 KiB
Markdown

# 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<String> violations(
GrpcPlatformProperties properties, // 자동 설정이 @EnableConfigurationProperties 로 가진다
GrpcMethodPolicyCatalog catalog, // 이 자동 설정에 빈 정의 없음
List<GrpcNamedChannelProfile> channelProfiles, // 빈 정의 없음
Set<String> stableModuleDependencies) // 이것을 런타임에 계산하는 코드가 저장소에 없음
```
넷 중 셋에 생산자가 없다. 특히 마지막은 "스타터가 해석한 모듈 id 집합" 인데, 그것을 실행 중에 산출하는 코드가 저장소 어디에도 없다 — 테스트는 `Set.of("grpc-core-api", "grpc-policy", "grpc-server", "grpc-client")` 리터럴을 넣는다. 검증기가 요구하는 입력을 구성 경계가 만들지 않으므로, 지금 형태로는 부를 수가 없다.
**수정.** 자동 설정에 검증기를 부르는 `InitializingBean`(또는 `SmartInitializingSingleton`) 빈을 하나 추가하되, 세 입력의 생산자를 함께 정한다.
- `GrpcMethodPolicyCatalog` · `List<GrpcNamedChannelProfile>``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
```