# grpc-advanced-bootstrap 완전 해부 > 상태: COMPLETE > 재오픈 게이트: cycle 2 — `src/main` production 9파일 610줄, test 2파일 279줄 축자 통독 완료. `STRUCTURAL_ONLY` 잔여 없음. > 기준 revision: `21234e38cdb9a926cbc92bb97a2aee2e4a7d2916` > 분석 범위: `src/grpc-advanced/grpc-advanced-bootstrap` > SSOT owner: `grpc-advanced-bootstrap` > integration/family document: `analysis/20-grpc-platform.md` (secondary, INTEGRATION_ONLY) --- ## 0. SSOT identity / 커버리지 - `allowed_dependencies`: `["grpc-core-api"]` — Advanced 는 Stable 공개 타입에 의존하고 그 반대는 없다 - `runtime_memberships`: **`[]`** — build-only | 파일 | LOC | 패키지 | |---|---:|---| | `GrpcAdvancedFeatureFlags` | 105 | bootstrap | | `GrpcAdvancedModuleGuard` | 84 | bootstrap | | `GrpcAdvancedCapability` | 65 | bootstrap | | `GrpcAdvancedCapabilityDisabledException` | 42 | bootstrap | | `GrpcCapabilityGrade` | 38 | bootstrap | | `GrpcAdvancedPromotionGate` | 85 | release | | `GrpcAdvancedPromotionEvidence` | 75 | release | | `GrpcAdvancedSupportMatrix` | 66 | release | | `GrpcAdvancedPromotionDecision` | 50 | release | | **main 합계** | **610** | | ### Coverage ledger | scope | count | disposition | reason | |---|---:|---|---| | `main/java/**` | 9 | `FULL_READ` | 610줄 전 본문 | | `test/java/**` | 2 | `FULL_READ` | 279줄(150+129) · 테스트 17개 | | `build.gradle` | 1 | `FULL_READ` | 12줄 | | `gradle.lockfile` | 1 | `STRUCTURAL_ONLY` | 잠금 파일 | `UNCLASSIFIED` 0. --- ## 1. 모듈의 정체 ```groovy // build.gradle:3-9 // The Advanced boundary itself: capability grades, the `ca-skeleton.grpc.advanced.*` feature-flag // contract, the module guard that refuses an unflagged capability, and the per-capability // promotion gate. // This leaf depends on Stable public types and never the other way round. ``` ## 2. 능력 15종과 등급 4종 능력을 하나씩 등급 매기는 것이 설계다. > "Bundling them under one 'advanced' flag makes enabling gRPC-Web — a compatibility bridge with a > proxy in front of it — the same decision as enabling xDS, which brings a control plane and its > outage modes. They are not the same decision, and a single switch is how the second one gets made > by accident." | 등급 | 시작 가능 | production 별도 승인 | |---|---|---| | `ADVANCED_STABLE` | 예 | 아니오 | | `EXPERIMENTAL` | 예 | **예** | | `WATCH` | 아니오 | — | | `DISABLED` | 아니오 | — | 기본 등급 분포는 `ADVANCED_STABLE` 11, `EXPERIMENTAL` 3(`HEDGING`·`CUSTOM_LOAD_BALANCER`·`XDS`), `WATCH` 1(`EDITION_2026`)이다. `EXPERIMENTAL` 에 두 번째 승인을 요구하는 근거가 적혀 있다. > "The flag says somebody wanted the feature; the approval says somebody accepted that its failure > modes are not fully characterised, which is a different person's decision on most teams." ## 3. 게이트가 세 조건을 순서대로 본다 ```java if (!flags.flagSet(capability)) → "its feature flag is not set" if (!grade.startable()) → "it is graded WATCH, which cannot start" if (production && requiresApproval && !approved) → "production needs a separate approval" ``` > "Collapsing them into one boolean produces a 'not enabled' message for three situations with three > different remedies." `requireStableStarterIsClean` 이 같은 불변식을 런타임에서도 확인한다 — 팻 자, 셰이드 산출물, 테스트 하네스처럼 다른 방식으로 조립된 런타임을 위해서다. **다만 그 메서드를 부르는 런타임이 없다**(§12.1). 지금 그 검사를 실행하는 것은 이 리프의 자기 테스트뿐이다. ## 4. 승격 게이트 증거는 능력마다 따로 기록된다. > "a shared record makes promoting one of them promote whichever others happened to be measured at > the same time." 일곱 항목(호환성·보안 검토·고장·성능·ADR·런북·실환경 테스트)과 담금 기간을 본다. 임계값이 둘이다. ``` ADVANCED_STABLE_SOAK = 7일 STABLE_DEFAULT_SOAK = 30일 ``` > "becoming a Stable default means every deployment gets it, which additionally puts its > dependencies on every classpath and its failure modes in every on-call rotation." 그리고 `WATCH` 는 `EXPERIMENTAL` 을 먼저 거쳐야 한다. `GrpcAdvancedSupportMatrix.apply` 는 결정의 시작 등급이 현재 등급과 다르면 거부한다 — 두 승격이 경합했거나 하나가 재생된 경우다. ## 10. 테스트 레인 두 테스트 279줄, 17개 테스트. 게이트의 세 조건, 능력별 개별 깃발, `WATCH` 거부, production 이중 승인, 활성 집합, 승격 임계값 둘, `WATCH` 선행 규칙, 매트릭스 경합 거부를 확인한다. `capabilitiesDraggedAlong` 이 항상 빈 목록을 돌려주고, 그 메서드가 존재하는 이유를 javadoc 이 적는다 — "the method exists so a test can assert that rather than a comment claiming it". 실제로 그 테스트가 있다. ## 12. negative-space probes **12.1 도달성.** Advanced 가족 전체가 배선되지 않는다. 리프 안에서도 절반만 쓰인다. 의존 선언은 다섯이다 — `grpc-advanced-diagnostics` · `-streaming` · `-compat` · `-edition` · `-resilience` 가 각각 `api project(':grpc-advanced:grpc-advanced-bootstrap')`. 그중 실제로 타입을 부르는 것은 둘뿐이다. | 호출 지점 | 무엇을 | |---|---| | `GrpcChannelDiagnosticsPolicy:47,53` | `GrpcAdvancedModuleGuard.available(flags, CHANNEL_DIAGNOSTICS)` · `…(flags, XDS)` | | `GrpcXdsStartupGuard:33` | `GrpcAdvancedModuleGuard.available(flags, XDS)` | `-streaming` · `-compat` · `-edition` 은 의존만 선언하고 참조가 없다. 그리고 리프 밖에서 불리는 것은 `available` **하나뿐**이다. - `GrpcAdvancedModuleGuard.require(...)` — 던지는 형태. 외부 호출자 0. 세 갈래 거부 메시지 전체가 자기 테스트에서만 실행된다. `GrpcAdvancedCapabilityDisabledException` 도 마찬가지다. - `requireStableStarterIsClean(...)` — 외부 호출자 0. javadoc 이 겨냥한 "다르게 조립된 런타임"이 이 검사를 부르지 않는다. - `advancedModules()` — 외부 호출자 0. - **`release` 패키지 4파일 276줄 전체** — 리프 밖 참조 0. 승격 게이트·증거·결정·지원 매트릭스를 만드는 곳이 자기 테스트 말고 없다. 즉 이 리프에서 실행 경로에 걸려 있는 것은 `GrpcAdvancedCapability` · `GrpcCapabilityGrade` · `GrpcAdvancedFeatureFlags` · `GrpcAdvancedModuleGuard.available` 네 조각이고, 나머지 절반은 선언이다. **12.2 대조군.** 능력을 하나씩 등급 매기는 형태가 messaging 의 `CompatibilityMatrix`(어댑터별 STABLE/EXPERIMENTAL/EXTENSION)와 같은 계열이다. 차이는 이쪽이 시작 가능 여부와 production 승인 요구를 등급 자체의 속성으로 둔 점이다. **12.4 드리프트.** build.gradle 이 서술한 네 요소(등급·깃발 계약·모듈 가드·승격 게이트)가 전부 존재한다. 드리프트 없음. ## 16. 확인하지 못한 것 - `verifyCleanArchitectureDependencies` 를 이 리비전에서 실행하지 않았다. - 등급 재정의로 `WATCH` 능력을 켜는 것을 실행으로 재현하지 않았다(§17.1). 코드 경로로 판정했다. - 테스트를 실행하지 않았다. 17개 전부 본문으로만 확인했다. §17.4 의 담금 역전도 `evaluate` 본문과 두 테스트가 고른 숫자(60일 · `DISABLED`)로 판정한 것이다. - `-streaming` · `-compat` · `-edition` 이 이 리프를 의존만 하고 쓰지 않는다는 것은 타입 이름 grep 으로 판정했다. 각 리프 SSOT 에서 다시 본다. ## 17. 손볼 것 ### 17.1 P3 — 등급 재정의에 하한이 없어 "켤 수 없다" 는 등급이 켜질 수 있다 `GrpcCapabilityGrade` 의 javadoc 이 두 등급을 단정한다. ``` WATCH — "Tracked, not implemented. Cannot be enabled." DISABLED — "Withdrawn or refused. Cannot be enabled." ``` 그런데 등급은 런타임에 갈아끼울 수 있다. ```java public GrpcAdvancedFeatureFlags withGrade(GrpcAdvancedCapability capability, GrpcCapabilityGrade grade) { grades.put(capability, grade); return this; } ``` `withGrade(EDITION_2026, ADVANCED_STABLE).enable(EDITION_2026)` 이면 가드의 두 번째 조건이 통과한다. 등급 올리기 자체는 의도된 기능이다 — 테스트 `a deployment may raise a capability's grade on its own evidence` 가 `HEDGING`(EXPERIMENTAL)을 `ADVANCED_STABLE` 로 올린다. 문제는 그 재정의에 하한이 없다는 것이다. - `EXPERIMENTAL` 을 올리는 것은 "실패 양식이 충분히 규명되지 않은 것을 감수한다" 는 판단이고 배포가 자기 증거로 내릴 수 있다. - `WATCH` 를 올리는 것은 다르다. 그 등급의 뜻이 "추적할 뿐 구현되지 않았다" 이므로 배포가 가질 자기 증거가 없다. 그리고 승격 게이트는 `WATCH` 가 `EXPERIMENTAL` 을 먼저 거쳐야 한다는 규칙을 갖는데, 런타임 재정의는 그 게이트를 지나지 않는다. 같은 리프 안에 문이 둘이고 증거 규칙은 한쪽에만 있다. 수정은 `withGrade` 가 현재 등급이 `startable()` 인 능력에만 적용되게 하거나, `WATCH`·`DISABLED` 에서 올리는 재정의를 거부하는 것이다. ### 17.2 P3 — 승격 게이트가 하향 전이도 승격 규칙으로 판정하고, javadoc 이 약속한 거부는 없다 ```java /** @throws IllegalArgumentException when the transition is not one this gate governs */ public static GrpcAdvancedPromotionDecision evaluate( GrpcAdvancedPromotionEvidence evidence, GrpcCapabilityGrade from, GrpcCapabilityGrade to) { … if (from == to) { throw new IllegalArgumentException("a promotion changes the grade"); } ``` 던지는 경우는 널과 `from == to` 둘뿐이다. "이 게이트가 다루는 전이가 아닐 때" 라는 조건에 해당하는 검사가 없다. 그래서 하향 전이가 승격 규칙으로 판정된다. ``` evaluate(none(XDS), ADVANCED_STABLE, DISABLED) → to != ADVANCED_STABLE 이므로 requiredSoak = 30일 → 증거 일곱 항목 부재 + 담금 부족으로 blockers 여덟 → 결정: 거부 ``` 능력을 철회하려는 결정이 증거 부족을 이유로 막힌다. 방향이 뒤집혀 있다. 지금은 도달성이 낮다 — 이 게이트를 부르는 production 코드가 없고 테스트도 상향 전이만 넣는다. 기록하는 이유는 javadoc 이 그 거부를 이미 약속했다는 점이다. 수정은 `to.ordinal()` 이 아니라 등급의 서열을 명시한 뒤 상향 전이만 받고 나머지는 던지는 것이다. 철회는 별도 경로가 필요하다. ### 17.3 P3 — 깃발 홀더가 가변이고 동기화가 없다 `GrpcAdvancedFeatureFlags` 는 두 `EnumMap` 을 `enable`·`withGrade` 로 갱신하고, `available`·`active` 가 같은 맵을 읽는다. `synchronized`·`volatile`·동시 자료구조가 없다. 시작 시 전부 설정하고 그 뒤로 읽기만 한다면 안전 공개 문제만 남는다. 다만 두 메서드가 `this` 를 돌려주는 유창한 형태라 런타임 중 갱신을 권하는 모양이고, `active()` 는 순회 중 갱신에 노출된다. 같은 저장소가 이 형태를 다른 리프에서 결함으로 기록했다(`GrpcCompletionReconciler` 의 동기화 없는 `ArrayList`). 여기서는 등급과 깃발이 요청 경로에서 읽히므로 같은 노출이 생길 수 있다. 수정은 홀더를 불변으로 만들고 `enable`·`withGrade` 가 새 인스턴스를 돌려주게 하는 것이다. 이 저장소가 다른 곳에서 쓰는 형태다(`GrpcProtoStyleManifest.allowingWellKnownTypes` 등). ### 17.4 P2 — 30일 담금이 열거형에 없는 등급을 위해 쓰였고, 그 결과 `WATCH → EXPERIMENTAL` 이 `→ ADVANCED_STABLE` 보다 어렵다 `GrpcAdvancedPromotionGate` javadoc 의 모형은 등급 둘이다. > "Reaching **Advanced Stable** means the capability works and is documented; becoming a **Stable > default** means every deployment gets it… The second needs the first plus a longer soak." 그리고 상수도 둘이다. ```java public static final Duration ADVANCED_STABLE_SOAK = Duration.ofDays(7); public static final Duration STABLE_DEFAULT_SOAK = Duration.ofDays(30); ``` 그런데 `GrpcCapabilityGrade` 의 값은 `ADVANCED_STABLE` · `EXPERIMENTAL` · `WATCH` · `DISABLED` 넷이다. **"Stable default" 라는 등급이 없다.** 선택은 이렇게 적혀 있다. ```java Duration requiredSoak = to == GrpcCapabilityGrade.ADVANCED_STABLE ? ADVANCED_STABLE_SOAK : STABLE_DEFAULT_SOAK; ``` `ADVANCED_STABLE` 이 아닌 **나머지 전부**가 30일 갈래로 떨어진다 — `EXPERIMENTAL`, `WATCH`, `DISABLED`. 존재하지 않는 등급을 위해 만든 갈래가 존재하는 세 등급을 삼켰다. **따라오는 역전.** `missing()` 검사도 목표 등급과 무관하게 일곱 항목을 전부 요구한다. 그래서: | 전이 | 필요한 증거 | 필요한 담금 | |---|---|---| | `EXPERIMENTAL → ADVANCED_STABLE` | 일곱 전부 | **7일** | | `WATCH → EXPERIMENTAL` | 일곱 전부 | **30일** | `WATCH` 능력이 밟도록 강제된 유일한 첫 걸음이(같은 메서드의 셋째 blocker: "it becomes EXPERIMENTAL before anything else") 상위 등급보다 엄격하다. 그리고 `WATCH` 의 뜻은 "추적할 뿐 구현되지 않았다" 이므로, 정의상 담금 기록이 가장 적은 등급에 가장 긴 담금을 요구한다. **테스트가 이 뒤틀림을 그대로 보여 준다.** ```java @DisplayName("becoming a Stable default needs a longer soak than becoming Advanced Stable") void theStableDefaultThresholdIsHigher() { … GrpcAdvancedPromotionGate.evaluate(weekLongSoak, GrpcCapabilityGrade.ADVANCED_STABLE, GrpcCapabilityGrade.DISABLED) // ← 철회 전이 .blockers() … .contains("requires 30"); } ``` 30일 갈래를 실행하려고 고른 전이가 `ADVANCED_STABLE → DISABLED`, 즉 **철회**다. 이름은 "Stable default 가 되는 것"이라고 말한다. 겨냥한 등급이 열거형에 없으니 그것을 밟을 방법이 없었고, 남은 것 중 아무거나 골라야 했다는 흔적이다. 그리고 `WATCH → EXPERIMENTAL` 을 확인하는 테스트는 담금을 60일로 준다. ```java GrpcAdvancedPromotionEvidence.complete(EDITION_2026, Duration.ofDays(60)) ``` 7일로 줬다면 통과하지 않는다. 30일 요구가 레인에 걸리지 않는 이유가 이 숫자 선택이다. **§17.2 와의 관계.** §17.2 는 이 갈래의 *증상* 하나(철회 전이가 승격 규칙으로 판정되는 것)를 기록했다. 원인은 목표 등급별 요구 사항이 없다는 것이고, 그래서 상향 전이 안에서도 순서가 뒤집혔다. **수정.** 목표 등급마다 요구 사항을 명시한다. ```java record Requirement(Set evidence, Duration soak) {} static Requirement requirementFor(GrpcCapabilityGrade to) { … } // EXPERIMENTAL 은 더 얕게 ``` `STABLE_DEFAULT` 를 실제로 표현하려면 등급으로 추가하거나(그러면 `GrpcAdvancedCapability` 를 떠나 Stable 기본값이 된다는 뜻이므로 별도 개념이 맞다) 이 게이트가 다루지 않는다고 적고 상수를 지운다. 지금은 이름만 있고 대상이 없다. ### 17.5 P3 — `capabilitiesDraggedAlong` 은 독립성을 증명하지 않는다. 상수를 상수와 비교한다 ```java /** Always empty, and the method exists so a test can assert that rather than a comment claiming it */ public static List capabilitiesDraggedAlong(GrpcAdvancedCapability promoted) { if (promoted == null) throw …; return List.of(); } ``` javadoc 이 스스로 밝히듯 본문은 무조건 빈 목록이다. 그것을 단언하는 테스트는 리터럴이 리터럴임을 확인한다 — 증거를 능력마다 따로 기록했다는 §4 의 설계 속성과는 아무 연결이 없다. 설계가 무너져 `apply` 가 다른 능력의 등급을 바꾸게 되어도 이 메서드는 여전히 빈 목록을 돌려준다. **진짜 증거는 같은 테스트의 다른 줄에 있다.** ```java matrix.apply(evaluate(complete(HEDGING, 7일), EXPERIMENTAL, ADVANCED_STABLE)); assertThat(matrix.gradeOf(HEDGING)).isEqualTo(ADVANCED_STABLE); assertThat(matrix.gradeOf(GRPC_WEB)).isEqualTo(webBefore); // ← 이 줄이 독립성을 붙든다 ``` 승격을 실제로 적용하고 다른 능력의 등급이 그대로임을 확인한다. 이쪽은 설계가 무너지면 깨진다. **이 문서의 이전 판정을 고친다.** 앞선 판에서 이 메서드를 "주석이 주장하는 대신 테스트가 붙든다"는 확인된 설계로 분류했다. 다시 읽으니 붙드는 것은 옆줄이고, 이 메서드는 그 옆줄이 있다는 사실을 가린다. **수정.** 메서드를 지우고 단언을 매트릭스 비교 쪽으로 남긴다. 남겨 둔다면 실제로 매트릭스를 훑어 등급이 바뀐 다른 능력을 돌려주게 만든다 — 그때 비로소 이름이 하는 말과 본문이 맞는다. ### 17.6 P3 — 예외가 들고 있는 능력이 `transient` 라 역직렬화 뒤 사라진다 ```java public class GrpcAdvancedCapabilityDisabledException extends RuntimeException { private static final long serialVersionUID = 1L; private final transient GrpcAdvancedCapability capability; … public GrpcAdvancedCapability capability() { return capability; } ``` `transient` 는 보통 직렬화 가능하지 않은 필드를 담은 `Serializable` 클래스에 대한 정적 분석 경고를 끄려고 붙인다. 그런데 열거형은 언제나 직렬화 가능하다 — 여기서 `transient` 가 막을 문제가 애초에 없다. 대가는 있다. 예외가 직렬화를 거쳐 오면 `capability()` 가 `null` 이다. 메시지 문자열은 살아남으므로 사람이 읽는 데는 지장이 없고, 그래서 눈에 띄지 않는다. **등급.** 이 예외를 던지는 `require` 자체가 리프 밖에서 불리지 않으므로(§12.1) 오늘 도달하지 않는다. `transient` 를 지우는 것이 수정 전부다. ### 확인된 설계(문제 아님) - **능력별 개별 등급과 개별 깃발** — 하나의 스위치가 두 번째 결정을 사고로 만들지 않는다. - **`EXPERIMENTAL` 의 production 이중 승인** — 원하는 사람과 감수하는 사람이 다르다. - **가드가 세 조건을 순서대로 보고 각각 다른 메시지를 내는 것.** - **증거를 능력마다 따로 기록한 것** — 공유 기록은 하나의 승격이 다른 것을 함께 올린다. - **두 담금 임계값** — 한 배포에서 일주일 돈 것과 모든 배포에 나가는 것은 같은 주장이 아니다. - **매트릭스가 시작 등급 불일치를 거부하는 것** — 경합과 재생을 구분해 준다. - **빈 컬렉션에 대한 `EnumSet.copyOf` 함정을 삼항으로 피한 것.** - **`GrpcAdvancedSupportMatrix.apply` 가 결정의 `from` 을 현재 등급과 대조하는 것** — 이 저장소가 같은 문제를 반대로 푼 자리가 있어서 대비된다. `grpc-codegen` 의 `GrpcSchemaArtifactPublisher.publish(candidate, decision)` 는 결정이 어느 후보를 판정한 것인지 확인하지 않아 짝이 어긋날 수 있다(그쪽 §17.4). 이쪽은 결정이 자기가 밟고 선 상태를 들고 있고 적용 시점에 대조한다 — 같은 형태의 올바른 판본이다. - **`ADVANCED_STABLE` 11 · `EXPERIMENTAL` 3 · `WATCH` 1 의 분포가 능력 성격과 맞는 것** — 제어 평면을 끌고 오는 `XDS`, 부하를 복제하는 `HEDGING`, 이름 해석을 갈아끼우는 `CUSTOM_LOAD_BALANCER` 만 이중 승인 대상이다. --- ## Source anchors ``` src/grpc-advanced/grpc-advanced-bootstrap/build.gradle:1-12 main/java/…/bootstrap/GrpcAdvancedFeatureFlags.java:1-105 main/java/…/bootstrap/GrpcAdvancedModuleGuard.java:1-84 main/java/…/bootstrap/GrpcAdvancedCapability.java:1-65 main/java/…/bootstrap/GrpcCapabilityGrade.java:1-38 main/java/…/bootstrap/GrpcAdvancedCapabilityDisabledException.java:1-42 main/java/…/release/GrpcAdvancedPromotionGate.java:1-85 main/java/…/release/GrpcAdvancedPromotionEvidence.java:1-75 main/java/…/release/GrpcAdvancedSupportMatrix.java:1-66 main/java/…/release/GrpcAdvancedPromotionDecision.java:1-50 test/java/…/release/GrpcAdvancedPromotionGateTest.java:1-150 test/java/…/bootstrap/GrpcAdvancedModuleGuardTest.java:1-129 grpc-advanced/grpc-advanced-diagnostics/…/GrpcChannelDiagnosticsPolicy.java:46-53 (available 소비) grpc-advanced/grpc-advanced-resilience/…/xds/GrpcXdsStartupGuard.java:33-38 (available 소비) ```