# grpc-server 완전 해부 > 상태: COMPLETE > 재오픈 게이트: cycle 2 — `src/main` production 17파일 축자 통독 완료. `STRUCTURAL_ONLY` 잔여 없음. > 기준 revision: `21234e38cdb9a926cbc92bb97a2aee2e4a7d2916` > 분석 범위: `src/grpc/grpc-server` > SSOT owner: `grpc-server` > integration/family document: `analysis/20-grpc-platform.md` (secondary, INTEGRATION_ONLY) --- ## 0. SSOT identity / 커버리지 - `runtime_memberships`: **`[]`** — build-only - vendor: `grpc-api`(BOM). Netty 의존 없음 — 프로파일은 설정 모델이지 배선이 아니다 | 파일 | LOC | |---|---:| | `GrpcServerInterceptorChain` | 110 | | `GrpcRawApiImportRule` | 108 | | `GrpcAdmissionController` | 103 | | `GrpcServiceAdapter` · `GrpcApplicationBoundaryRules` | 92 · 92 | | `GrpcServerInterceptorOrder` | 90 | | `GrpcServerProfile` · `GrpcNettyParityContract` · `GrpcExecutorProfile` | 84 · 69 · 66 | | `GrpcNettyVariantSelector` · `GrpcServerInterceptorStage` · `GrpcServiceAdapterDescriptor` | 52 · 51 · 48 | | `GrpcServerTransport` · `GrpcNettyVariant` · `GrpcApplicationInvocation` · `GrpcServiceAdapterMarker` · `GrpcResponseMapper` | 38 · 32 · 28 · 25 · 18 | | test 5파일 | 730 | ### Coverage ledger | scope | count | disposition | reason | |---|---:|---|---| | `main/java/**` | 17 | `FULL_READ` | 1,106줄 전 본문 | | `test/java/**` | 5 | `FULL_READ` | 730줄 | | `build.gradle` | 1 | `FULL_READ` | 전문 | | `gradle.lockfile` | 1 | `STRUCTURAL_ONLY` | 잠금 파일 | `UNCLASSIFIED` 0. --- ## 1. 모듈의 정체 ```groovy // build.gradle:3-6 // Server boundary: the ArchUnit-shaped application boundary rules, the typed service adapter SPI, // the interceptor order contract, and the Netty server/executor/admission profiles. // // The Netty profiles are configuration models, not Netty wiring — no netty dependency here. Real // Netty lives in `grpc-testkit`'s certification lane, which is where transport evidence is produced. ``` ## 2. 인터셉터 순서 계약 열 단계이고 선언 순서가 계약이다. 각 위치의 이유가 열거형 javadoc 에 있다. ``` EXCEPTION_BOUNDARY → TRACE → AUTHENTICATION → ACTOR_TENANT → AUTHORIZATION → ADMISSION → DEADLINE_CANCELLATION → IDEMPOTENCY → VALIDATION → SERVICE_ADAPTER ``` - 예외 경계가 가장 바깥 — 이후 단계의 실패가 매핑되지 않은 상태로 새지 않는다 - 인증 → 행위자·소속 → 인가 — 각 단계가 앞 단계의 답을 필요로 한다 - 승인이 마감보다 먼저 — 부하 중 서버가 일을 쓰기 전에 흘려보낸다 - 멱등이 검증보다 먼저 — 재생된 요청이 이미 받아들인 본문을 다시 검증하지 않고 저장된 결과를 돌려준다 - 검증이 어댑터 직전 — 사용 사례는 믿을 수 있는 메시지를 받는다 필수가 아닌 단계는 멱등 하나다 — 상태 변경 키 메서드가 없는 서버에는 할 일이 없기 때문이다. ## 3. 뒤집기가 이 클래스의 존재 이유다 > "`ServerInterceptors.intercept` wraps each interceptor around the previous one, so the last one > passed is the outermost at runtime — the opposite of how the order reads. Every codebase that > builds this list by hand gets it backwards at least once, and the symptom is an exception boundary > that catches nothing." `inStableOrder()` 와 `inGrpcRegistrationOrder()` 를 나누고, 후자가 전자의 역순임을 테스트가 붙든다. ## 4. 순서 검증의 근거 > "A chain with validation before authentication lets an anonymous caller probe the schema through > error messages; one with the exception boundary in the middle lets a throwable from an earlier > stage escape as `UNKNOWN`. Neither shows up in a test of the happy path." 네 규칙이다 — 중복 단계, 필수 단계 누락, 역순, 예외 경계가 최외곽이 아님. ## 5. 원시 API 차단 규칙 금지 타입 열네 개가 채널·서버·호출을 손으로 만드는 구성 API 다. 금지가 아니라 허용 패키지 목록을 받는다. > "They are legitimate inside the platform and inside generated code, which is why this rule takes > an allowlist of packages rather than banning them outright." 그리고 허용 목록이 비면 생성자가 거부한다 — 플랫폼 자신은 어딘가에서 채널을 만들어야 한다. ## 6. 응용 경계 규칙 금지 접두 열세 개와 금지 타입 셋. 접두만으로 너무 넓은 경우를 위해 정확한 타입 목록을 따로 둔다. > "a transport adapter that calls a repository has moved the use case into the transport, and the > next caller of that use case — a scheduled job, a message consumer — either duplicates it or > reaches through the controller." ## 10. 테스트 레인 다섯 테스트. 순서 계약(등록 역순 포함), 프로파일 거부(무제한 큐·in-process production·킵얼라이브·연결 수명), 승인 경계, 어댑터의 매핑, 경계 규칙과 원시 API 규칙을 확인한다. ## 12. negative-space probes **12.1 도달성.** 이 리프의 다섯 타입은 저장소 어디에서도(자기 리프 밖) 참조되지 않는다. ``` GrpcApplicationBoundaryRules · GrpcRawApiImportRule · GrpcServiceAdapterMarker GrpcServerInterceptorChain · GrpcNettyParityContract → leaf 밖 참조 0 ``` `GrpcAdmissionController`·`GrpcExecutorProfile`·`GrpcServerProfile` 은 `grpc-spring-boot-starter` 가 빈으로 만들고, `GrpcAdmissionController` 는 `grpc-admin` 의 배수 조정자가 협력자로 받는다. 다만 **만들어지는 것과 불리는 것은 다르다.** 재통독에서 다시 세었다. ``` tryAdmit() production 호출 0 (테스트 3곳) release() production 호출 0 promoteFromQueue() production 호출 0 ``` 즉 승인 제어기는 빈으로 존재하고 아무 호출도 승인받지 않는다. 그것을 부를 자리인 `ADMISSION` 인터셉터 단계의 구현이 이 가족에 없기 때문이다(§12.1 의 `GrpcServerInterceptorChain` 미참조와 같은 원인). §17.4 가 그 첫 호출자가 만나게 될 것을 다룬다. **12.4 드리프트.** build.gradle 이 서술한 네 요소가 전부 존재하고, Netty 의존이 없다는 서술도 맞다. ## 16. 확인하지 못한 것 - 실제 서버를 세워 인터셉터 사슬을 돌리지 않았다. 이 리프에 서버를 만드는 코드가 없다. - `GrpcNettyParityContract` 가 서술하는 두 변형의 동등성을 실행으로 확인하지 않았다. 그 클래스의 `unproven(...)` 을 부르는 코드도 저장소에 없다 — grpc-testkit §17.5 와 같은 형태의 평가기다. - §17.4 의 경합을 실행으로 재현하지 않았다. 읽기와 증가가 분리되어 있다는 것과 하한 가드가 없다는 것으로 판정했다. - `gradle.lockfile` 은 읽지 않았다(`STRUCTURAL_ONLY`). ## 17. 손볼 것 ### 17.1 P2 — 두 아키텍처 규칙이 저장소 소스에 적용되지 않는다 `GrpcApplicationBoundaryRules` javadoc: > "The list is package prefixes rather than a prose rule, **so it can be applied by an architecture > test, by a source scan and by a review checklist** without three people deciding what 'must not use > a repository' covers." 세 적용처 중 저장소에 존재하는 것이 없다. ``` GrpcApplicationBoundaryRules leaf 밖 참조 0 GrpcRawApiImportRule leaf 밖 참조 0 GrpcServiceAdapterMarker leaf 밖 참조 0 (규칙이 어댑터를 열거하려고 만든 마커) ``` 그리고 이 리프의 테스트는 저장소 파일을 훑지 않는다. 인라인 소스 문자열을 넣는다. ```java assertThat(rule.violations("DocumentClient.java", applicationSource)).isNotEmpty(); assertThat(rule.violations("ChannelFactory.java", platformSource)).isEmpty(); ``` 즉 규칙의 판정 로직은 검증되지만, 저장소의 어떤 파일도 그 판정을 받지 않는다. `GrpcServiceAdapterMarker` 는 규칙이 어댑터를 런타임에 열거할 수 있도록 만든 애너테이션인데, 그것을 붙인 타입도 그것을 읽는 코드도 없다. **수정.** 이 리프의 테스트에 저장소 소스를 훑는 검사를 추가한다 — `src/**/*.java` 를 읽어 `GrpcRawApiImportRule.violations` 를 돌리고 비어 있음을 단언하는 형태다. 규칙이 이미 파일 이름과 소스 텍스트를 받는 서명이므로 재료는 갖춰져 있다. ### 17.2 P3 — 원시 API 규칙이 import 문만 보므로 완전 수식 사용과 와일드카드를 놓친다 ```java private static final Pattern IMPORT = Pattern.compile("^\\s*import\\s+(?:static\\s+)?([\\w.]+)\\s*;", MULTILINE); … if (RAW_API_TYPES.contains(imported)) { violations.add(…); } ``` 두 형태가 빠진다. ```java io.grpc.ManagedChannelBuilder.forAddress("h", 1).build(); // import 없이 완전 수식 import io.grpc.*; // 정확 일치 실패 ``` 이것이 가정에 그치지 않는 이유는 이 저장소 자신의 문체다. 같은 가족의 여러 파일이 완전 수식 참조를 본문에 그대로 쓴다. ``` GrpcConsumerFixture java.util.regex.Pattern.compile(...) GrpcAdvancedSupportMatrix java.util.stream.Collectors.toUnmodifiableMap(...) GrpcProtoStyleManifest java.util.Set / java.util.LinkedHashSet 인라인 ``` 즉 이 코드베이스에서 완전 수식 사용은 예외가 아니라 흔한 형태다. 규칙 클래스의 자바독은 "there is nothing to reach for" 를 목표로 든다. 지금 형태는 손이 닿는 경로 하나만 본다. 수정은 정규식을 타입 이름의 등장 자체로 넓히거나(오탐이 생기므로 주석·문자열 제거가 필요), 바이트코드 기반 검사로 옮기는 것이다. 후자가 이 저장소의 다른 아키텍처 게이트와 형태가 같다. ### 17.3 P3 — 빌더 경로에서 순서 규칙 넷 중 셋이 발화할 수 없다 ```java public GrpcServerInterceptorChain build() { GrpcServerInterceptorOrder.requireStableOrder(List.copyOf(byStage.keySet())); … } ``` `byStage` 는 `EnumMap` 이므로 `keySet()` 은 언제나 열거형 선언 순서다. 그리고 `stage(...)` 가 같은 단계의 두 번째 등록을 이미 거부한다. 따라서 빌더가 만드는 목록에서는 중복도, 역순도, 예외 경계가 최외곽이 아닌 경우도 발생할 수 없다. 발화 가능한 규칙은 필수 단계 누락 하나다. 결함은 아니다 — 나머지 셋은 `violations(List)` 를 직접 부르는 외부 호출자를 위한 것이고, 테스트가 그 경로로 셋을 모두 확인한다. 기록하는 이유는 빌더를 쓰는 조립 코드가 그 셋의 보호를 받는다고 읽기 쉽기 때문이다. 실제 보호는 자료구조가 준다. ### 17.4 P2 — 승인 제어기의 세 메서드가 원자적이지 않고, 큐 계수기를 되돌리는 경로가 없다 이 리프가 SSOT 이므로 여기에 적는다. `grpc-policy` §17.1 이 이 클래스를 대조군으로 지목하는데, 지목된 쪽 문서에 판정이 없었다. **첫째, 읽고 나서 따로 증가시킨다.** ```java public Decision tryAdmit() { int running = inFlight.get(); if (running < maxConcurrentCalls) { inFlight.incrementAndGet(); // ← 읽기와 증가 사이에 다른 스레드가 들어온다 return new Decision(true, …); } int waiting = queued.get(); if (waiting < maxQueuedCalls) { queued.incrementAndGet(); // ← 같은 형태 … ``` 경계에 있는 N 개 스레드가 모두 통과한다. `AtomicInteger` 를 쓰면서 비교와 증가를 나눈 형태이고, 같은 가족의 정본이 `GrpcRetryBudget.tryConsume` 의 비교 후 교체 루프다. `release()`·`promoteFromQueue()` 도 같다 — `get() > 0` 을 확인한 뒤 별도로 감소시키므로, 두 스레드가 같은 마지막 하나를 보고 둘 다 감소시켜 음수가 될 수 있다. 클래스가 `Math.max(0, …)` 같은 하한도 두지 않는다. **둘째, 큐 계수기를 되돌리는 경로가 없다.** 큐에 들어간 호출도 `admitted=true` 를 받는다. 그런데 그 경로는 `queued` 만 올리고 `inFlight` 는 올리지 않는다. 그리고 끝난 호출을 반납하는 메서드는 하나뿐이다. ```java public void release() { if (inFlight.get() > 0) { inFlight.decrementAndGet(); } // ← queued 는 건드리지 않는다 } ``` 따라서 호출자가 `promoteFromQueue()` 를 정확히 한 번 끼워 넣지 않으면 계수기가 어긋난다 — 큐에서 실행된 호출이 끝나면 `queued` 는 그대로이고 `inFlight` 만 줄어든다. `releaseQueued()` 같은 메서드도, 그 짝짓기를 요구하는 서술도 없다. **시험이 이것을 볼 수 없는 이유.** 두 시험 모두 단일 스레드이고, `releaseAndPromotionTrackCapacity` 는 `release()` 와 `promoteFromQueue()` 를 **짝지어** 부른다. 짝짓지 않는 경로는 시험되지 않는다. **등급.** 오늘 호출자가 없으므로(§12.1) P2. 승인 단계를 배선하는 순간 P1 이다 — 부하 아래에서 경계가 새는 것과, 큐 계수기가 단조 증가해 `at capacity` 가 영구히 참이 되는 것이 함께 온다. **수정.** 세 메서드를 비교 후 교체 루프로 바꾸고, 큐 경로에 대응하는 반납 메서드를 두거나 `promoteFromQueue` 를 `release` 안으로 접는다. ### 확인된 설계(문제 아님) - **열 단계의 순서와 각 위치의 이유를 열거형 javadoc 에 적은 것.** - **등록 순서 뒤집기를 클래스로 분리하고 그 이유를 적은 것.** - **멱등만 선택 단계로 둔 것.** - **순서 위반의 증상이 정상 경로 테스트에 나타나지 않는다는 근거.** - **원시 API 를 금지가 아니라 허용 패키지 목록으로 다룬 것.** - **허용 목록이 빈 규칙을 거부한 것.** - **접두 목록으로 너무 넓은 경우를 위해 정확한 타입 목록을 따로 둔 것.** - **Netty 의존 없이 프로파일만 두고, 실제 전송 증거를 테스트킷 인증 레인으로 넘긴 것.** --- ## Source anchors ``` src/grpc/grpc-server/build.gradle:1-14 main/java/…/server/GrpcServerInterceptorChain.java:1-110 main/java/…/server/GrpcServerInterceptorOrder.java:1-90 main/java/…/server/GrpcServerInterceptorStage.java:1-51 main/java/…/architecture/GrpcRawApiImportRule.java:1-108 main/java/…/architecture/GrpcApplicationBoundaryRules.java:1-92 main/java/…/architecture/GrpcServiceAdapterMarker.java:1-25 main/java/…/server/(GrpcAdmissionController · GrpcServiceAdapter · GrpcServerProfile · GrpcExecutorProfile · GrpcNettyParityContract · GrpcNettyVariantSelector · GrpcServiceAdapterDescriptor · GrpcServerTransport · GrpcNettyVariant · GrpcApplicationInvocation · GrpcResponseMapper) test/java/…/(architecture/GrpcApplicationBoundaryRulesTest · server/GrpcServerInterceptorOrderTest · server/GrpcServerProfileTest · server/GrpcServiceAdapterTest · server/GrpcNettyVariantSelectorTest) ```