Files
document-haness/docs/clean-architecture-backend-template/analysis/16-adapter-inbound-graphql.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

1338 lines
108 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# adapter-inbound-graphql — 코드베이스 분석
## SSOT identity — 2026-08-31 재검증
- registered leaf id: `adapter-inbound-graphql`
- canonical state `analysisFile`: `analysis/16-adapter-inbound-graphql.md` (이 문서) — 이 leaf의 단일 SSOT
- source path: `src/adapter/inbound/graphql` · Gradle `:adapter:inbound:graphql`
- registry `allowed_dependencies`: `["domain-core", "application-core", "shared-contract"]`
- registry `runtime_memberships`: `["app-bootstrap"]`
- coverage ledger: `FULL_READ` **534** / `STRUCTURAL_ONLY` **0** / `EXCLUDED` **0** / `UNCLASSIFIED` **0**
- 최초 분석 revision `a24ece9c` → 재검증 revision `21234e38` · 이 리프의 변경 파일 **0**
- 재검증 증거: `EVD-333`(소스 드리프트 0), `EVD-334`(lane 재실행)
> 재검증이 확인한 것은 대상이 움직이지 않았다는 사실이지, 아래 서술이 옳다는 보증이 아니다.
> 이번 사이클에서 코드에 대고 다시 확인한 항목은 이 문서의 검증 절과 위 증거가 가리키는 범위다.
---
> **분석 대상** `src/adapter/inbound/graphql` · revision `a24ece9cf797f7ea647e33bf846b115208ed1ba5`
> **분모** 534 tracked files (main 411 · test 103 · testFixtures 16 · governance 4)
> **LOC** main Java 26,303 · test Java 13,671
> **근거** `evidence/raw/205-inbound-graphql-module-inventory.txt`, 패키지 도달성 지도 `206-...`
## 0. 이 모듈의 형태
main 411 파일 26,303 LOC로 inbound-web(400 / 27,473) 다음으로 크다. 그러나 **조립 구조가 근본적으로 다르다.**
컴포지션 루트는 이 leaf를 컴포넌트 스캔에서 통째로 제외한다:
```java
// CaSkeletonApplication.AUTO_CONFIGURED_PACKAGES
|dev\.caskeleton\.adapter\.inbound\.graphql\..*
```
그래서 이 leaf의 어떤 `@Controller``@Component`도 스캔으로 발견되지 않고, 조립은 전적으로 자동설정 진입점 하나에 달려 있다:
```
META-INF/spring/...AutoConfiguration.imports
dev.caskeleton.adapter.inbound.graphql.autoconfigure.GraphQlRootAutoConfiguration
META-INF/spring.factories
AutoConfigurationImportFilter = ...GraphQlOffAutoConfigurationImportFilter
EnvironmentPostProcessor = ...GraphQlActivationEnvironmentPostProcessor
```
`app-bootstrap``sample-portfolio`가 이 leaf에서 import하는 타입은 **0개**다(도달성 지도 `ext` 열 전부 0). 배선이 전부 leaf 안에 있다.
**그리고 이 leaf는 inbound-web이 겪고 있는 결함을 이미 한 번 겪고 고쳤다.** `GraphQlRootAutoConfiguration``@Import` 목록에 달린 주석이 그것이다:
> "The resolver for the schema's only field. It is a `@Controller` in a package the composition root's component scan excludes by regex — the exclusion that makes this capability optional — and **no root imported it**, so a deployment with GraphQL on served a schema declaring `_health: String!` with nothing to resolve it. Every query answered `NullValueInNonNullableField`. **Its own tests passed throughout by registering the class themselves, which is the shape of the defect rather than a defence against it.**"
inbound-web §8.1에서 확인한 상태 — 스캔에서 빼고 자동설정이 넘겨받지 않아 컨트롤/핸들러가 사라진 상태, 그리고 테스트가 그것을 스스로 등록해 통과하는 상태 — 를 이 모듈은 이름 붙여 진단하고 닫았다.
## 1. 커버리지 원장
| # | sub-scope | main | test | 기타 | 합 | 상태 |
|---|---|---:|---:|---:|---:|---|
| 1 | governance + `autoconfigure` + `moduleboundary` + `architecture` + `api` | 35 | 21 | 4 | 60 | **COMPLETE** |
| 2 | `schema` + `scalar` + `compat` | 37 | 9 | | 46 | **COMPLETE** |
| 3 | `execution` + `context` + `runtime` | 48 | 12 | | 60 | **COMPLETE** |
| 4 | `cost` + `policy` + `security` | 45 | 12 | | 57 | **COMPLETE** |
| 5 | `http` + `error` + `observation` | 38 | 10 | | 48 | **COMPLETE** |
| 6 | `dataloader` + `fetch` + `pagination` + `mutation` | 58 | 11 | | 69 | **COMPLETE** |
| 7 | `release` | 9 | 1 | | 10 | **COMPLETE** |
| 8 | `advanced/` 스트리밍 (`subscription`·`websocket`·`sse`·`incremental`·`rsocket`) | 45 | 6 | | 51 | **COMPLETE** |
| 9 | `advanced/` 요청 성형 (`persisted`·`get`·`replay`·`chaining`·`admin`) | 46 | 7 | | 53 | **COMPLETE** |
| 10 | `advanced/` 스키마·플랫폼 (`federation`·`composition`·`codegen`·`springdata`·`security`·`release`·`bootstrap`) | 50 | 9 | | 59 | **COMPLETE** |
| 11 | `testFixtures` + test 잔여 | 0 | 5 | 16 | 21 | **COMPLETE** |
| | **TOTAL** | **411** | **103** | **20** | **534** | **11 / 11** |
분할은 `205-...`의 패키지 트리에서 기계 계산했고 중복 0 · 미할당 0이다.
---
# Sub-scope 01 — governance + `autoconfigure` + `moduleboundary` + `architecture` + `api` (60 files, main 35 + test 21 + governance 4)
> 내부 상태: COMPLETE — **60 / 60 FULL_READ** · 근거 `evidence/raw/207-inbound-graphql-autoconfigure-probes.txt` (`file_count=60`)
## 2. 무엇을 하는 코드인가
**조립 진입점이 하나다.** `.imports`에 등록된 것은 `GraphQlRootAutoConfiguration` — 마스터 게이트를 든 얇은 루트이고, 실제 39개 빈은 그것이 `@Import`하는 `GraphQlPlatformAutoConfiguration`(704줄, `@ConditionalOn*` 43개)에 있다. 그 분리의 이유가 테스트로 고정돼 있다:
```java
// GraphQlPlatformAutoConfigurationTest.theAutoConfigurationIsRegisteredInTheImportsMetadata
.as("the registered entry is the master-gated root, not the platform configuration it imports; "
+ "registering the platform directly is what let a context assemble a GraphQL endpoint that no switch had asked for")
.contains(GraphQlRootAutoConfiguration.class.getName())
.doesNotContain(GraphQlPlatformAutoConfiguration.class.getName() + "\n");
```
**off 계약이 빈 부재만으로 성립하지 않는다는 것을 알고 있다.** Spring GraphQL은 자기 자동설정에서 `/graphql`을 발행하므로 프로젝트 빈을 하나도 만들지 않아도 엔드포인트가 열린다. 그래서 `spring.factories``AutoConfigurationImportFilter`를 하나 더 건다:
> `GraphQlOffAutoConfigurationImportFilter` — "The GraphQL starter contributes its auto-configurations through Boot's import metadata, so an ordinary `@EnableAutoConfiguration` application publishes `/graphql` from the classpath alone, whatever any project condition says. That is the difference between an endpoint that is off and one whose project beans are absent while the framework serves it anyway. **A bean-inventory assertion cannot see a route the framework published.**"
프레임워크 자동설정 10개를 이름으로 열거해 마스터가 꺼져 있으면 후보 집합에서 제거한다.
**환경 후처리기가 두 가지를 부팅 전에 정리한다.** `GraphQlActivationEnvironmentPostProcessor`(`Ordered.LOWEST_PRECEDENCE`, 프로파일 설정이 이미 기여된 뒤에 도는 이유까지 적혀 있다):
1. **은퇴한 안전 키를 무시가 아니라 거부.** `backend.graphql.production``backend.graphql.environment`는 더 이상 record 컴포넌트가 아니고 Spring 바인더는 모르는 키를 조용히 넘긴다. `GraphQlRetiredSafetyAxis`가 그것을 부팅 실패로 바꾸는 이유가 정확하다 — "an operator who sets the key they have always set gets a clean startup and **a silently different safety posture** — which is a worse outcome than the split-brain being fixed, because the old configuration at least did something."
2. **프레임워크 콘솔 플래그에 플랫폼 기본값을 최저 우선순위로 기여.** Spring Boot는 introspection을 기본 허용하고 이 플랫폼은 허용하지 않아서, "turning GraphQL on failed at startup on a contradiction nobody had configured."
**`GraphQlPlatformStartupValidator`가 12개 거부 규칙을 든다** — GraphiQL·introspection의 배포 모드별 금지, 서명 없는 커서 거부("unsigned cursors are client-editable"), 페이지/복잡도 상한 양수, 미지원 기능 4종(멀티파트 업로드 · HTTP 배열 배칭 · 요청 범위 트랜잭션 · 응답 캐싱), Stable 스타터가 Advanced를 켜는 것 금지.
## 3. Negative-space probes — sub-scope 01
### 3.1 (8.1) 도달성 — 컴포지션 루트와의 관계
```
$ grep -n 'graphql' CaSkeletonApplication.java
|dev\.caskeleton\.adapter\.inbound\.graphql\..* <- 컴포넌트 스캔 제외
$ grep -rn 'inbound.graphql' app-bootstrap/src/main sample-portfolio/src/main -> 0
```
leaf 전체가 컴포넌트 스캔 밖이고 외부에서 import하는 타입이 0개다. 조립은 `.imports` 한 줄과 `spring.factories` 두 줄이 전부다.
**이 구조에서 정확히 무엇이 잘못될 수 있는지를 이 leaf가 이미 겪고 적어 두었다:**
```java
// GraphQlRootAutoConfiguration @Import 주석
// The resolver for the schema's only field. It is a @Controller in a package the composition
// root's component scan excludes by regex — the exclusion that makes this capability optional —
// and no root imported it, so a deployment with GraphQL on served a schema declaring
// `_health: String!` with nothing to resolve it. Every query answered NullValueInNonNullableField.
// Its own tests passed throughout by registering the class themselves, which is the shape of the
// defect rather than a defence against it.
```
`HealthGraphqlController`가 지금 `@Import` 목록에 있다. **이것은 inbound-web §8.1과 같은 결함이고, 여기서는 진단되어 닫혔다.** 두 모듈의 차이는 규모다 — web에서는 같은 형태가 다섯 패키지 23개 파일에 대해 열려 있다.
### 3.2 (8.2) 조건 형제 비교 — off 계약의 두 절반
| 절반 | 무엇을 막는가 | 검증 |
|---|---|---|
| `GraphQlRootAutoConfiguration``@ConditionalOnProperty` | 이 leaf의 39개 빈 | `GraphQlShippedAndGatedTest.graphQlOffHoldsNothing` — 빈 인벤토리 |
| `GraphQlOffAutoConfigurationImportFilter` | 프레임워크가 발행하는 `/graphql` 라우트 | `GraphQlShippedAndGatedTest.graphQlOffPublishesNoEndpoint` — 실제 포트 |
두 번째 테스트가 특히 정교하다. 리터럴 404를 단언하지 않고, 매핑된 적 없는 경로의 상태 코드와 **같은지**를 본다 — 주석이 그 이유를 적는다: "Asserting a literal 404 would have been wrong: the security filter chain runs before ...". 그리고 그 테스트는 이 leaf가 아니라 app-bootstrap에 있다. off 계약은 출하 조립에서만 검증할 수 있으므로 옳은 위치다.
### 3.3 (8.3) 중복 메커니즘 — 마스터 스위치를 읽는 세 지점
`backend.graphql.enabled`를 읽는 곳이 셋이다: `GraphQlRootAutoConfiguration``@ConditionalOnProperty`, `GraphQlOffAutoConfigurationImportFilter.match`, `GraphQlRetiredSafetyAxis.problems`. 셋 다 문자열 리터럴로 키를 갖는다.
같은 키를 세 곳이 문자열로 갖는 것은 드리프트 위험이지만, 세 지점이 서로 다른 생애주기(자동설정 조건 · import 필터 · 환경 후처리)에 있어 공유 상수를 두기 어렵다. 그리고 셋 중 하나라도 어긋나면 `GraphQlShippedAndGatedTest`의 두 단언 중 하나가 깨진다. 중복이되 검증으로 묶여 있다.
### 3.4 (8.4) 문서/카운트 드리프트 — 하드코딩된 프레임워크 자동설정 목록
`GraphQlOffAutoConfigurationImportFilter`가 프레임워크 자동설정 **10개**를 문자열 상수로 열거한다(Boot 4의 `org.springframework.boot.graphql.autoconfigure.*` 배치). javadoc이 위험을 스스로 밝힌다:
> "A misspelled entry fails open silently — the filter simply never matches — so **the test that protects this asserts a 404 on the real port** rather than checking what this method returns."
그 테스트는 존재한다(§3.2). §4.1.
## 4. Sub-scope 01 findings
### 4.1 P3/기록 — 프레임워크 자동설정 목록이 하드코딩이고 드리프트 검사가 부분적이다
`GraphQlOffAutoConfigurationImportFilter`의 10개 이름은 Spring Boot 버전에 묶인 문자열이다. 보호 장치인 `graphQlOffPublishesNoEndpoint``/graphql` 경로 하나를 실제 포트에서 확인하므로 다음을 잡는다:
- 목록의 오타나 이름 변경으로 `GraphQlWebMvcAutoConfiguration`이 통과하는 경우 → `/graphql`이 열리고 테스트가 깨진다.
그리고 다음은 잡지 못한다:
- **Boot 업그레이드가 새 GraphQL 자동설정을 추가**하고 그것이 `/graphql` 이외의 경로(예: RSocket 라우트, SSE 엔드포인트, 새 콘솔 경로)를 발행하는 경우. 목록에 없으므로 필터를 통과하고, 테스트는 그 경로를 조회하지 않는다.
`spring.factories`에 등록된 필터라는 특성상 fail-open이 기본값이라는 점을 javadoc이 인정하고 있으므로 은폐는 아니다. 기록하는 것은 보호의 범위다 — 목록이 열 개이고 확인되는 경로가 하나다.
**닫는 방법** — 클래스패스의 `AutoConfiguration.imports`에서 `graphql`을 포함하는 후보를 읽어 하드코딩 목록과 대조하는 테스트를 추가하면, 새 항목이 추가되는 순간 목록을 갱신하도록 강제된다. 이 leaf는 이미 같은 형태의 고정을 두 번 쓰고 있다(`theAutoConfigurationIsRegisteredInTheImportsMetadata`, `everyPlatformPropertyAppearsInTheGeneratedConfigurationMetadata`).
### 4.2 — 그 외 결함 없음
35개 main 파일 중 조립·활성화·검증 경로가 전부 닫혀 있고, 각 결정에 그것이 막는 구체적 실패가 적혀 있다. 은퇴 키 거부, 프레임워크 플래그 기본값 기여, 마스터 게이트를 든 얇은 루트, off 계약의 두 절반과 그 각각의 테스트 — 이 sub-scope에서 미도달이거나 미검증인 장치는 없다.
## 5. Sub-scope 01 완료 조건
- denominator 60 / 60 FULL_READ (probe가 `file_count=60` 확인)
- §8.1~§8.4 수행 — 도달성(조립 진입점 전수) · off 계약 두 절반 비교 · 마스터 키 삼중 참조 · 하드코딩 목록 드리프트
- 기록 1건, 결함 0건
- 소스 미변경
---
# Sub-scope 02 — `schema` + `scalar` + `compat` (46 files, main 37 + test 9)
> 내부 상태: COMPLETE — **46 / 46 FULL_READ** · 근거 `evidence/raw/208-inbound-graphql-schema-probes.txt`
## 6. 무엇을 하는 코드인가
**`schema` (19)** — 스키마 거버넌스. 매핑 검사 게이트, `@oneOf` 규칙, 스칼라 매니페스트, SDL 조립과 계약 정체성.
이 패키지의 논증이 이 모듈에서 가장 촘촘하다:
- `GraphQlMappingInspectionGate` — Spring의 스키마 매핑 검사를 보고서에서 **게이트로** 승격한다. 실행 시점도 근거가 있다: "Runs after all controller, scalar and type-resolver wiring is registered — inspecting earlier would report resolvers that simply had not been contributed yet." 그리고 `GraphQlMappingPolicy`가 그 승격 이유를 적는다 — "A silently unmapped field returns `null` at runtime instead of ..." — §3.1에서 본 `HealthGraphqlController` 사고의 형태다.
- `GraphQlMappingIssue` — 스키마 좌표와 선언 리소스만 담고 그 외에는 아무것도 담지 않는다: "A mapping report runs at startup and lands in logs, so it must never carry request or credential content."
- `GraphQlScalarDefinition``Upload` 스칼라를 **생성자에서 거부**한다: "the platform does not implement GraphQL multipart upload, and binary lifecycle belongs to the Fileserver capability, so an `Upload` scalar could only ever be a half-working promise."
- `GraphQlScalarManifest` — 중복 스칼라 이름을 거부한다: "two declarations of one scalar mean two coercions and the wiring order would silently pick a winner."
- `GraphQlSchemaContract` — 스키마 해시 하나로 호환성을 판정하지 않는다. SDL 바이트가 같아도 파괴적 변경 규칙·스칼라 강제 변환·디렉티브 의미가 바뀌었으면 외부 계약이 다르므로 **네 부분 전부**를 비교하고, `sameSchemaBytes`는 "available for cache keying but is explicitly not a compatibility verdict"로 분리한다.
**`scalar` (7)** — `BigDecimal`(211) · `Long`(189) · `Instant`(117) · `Uuid`(109) · `Date`(105) 강제 변환과 `GraphQlDecimalBounds`. `GraphQlScalarWiringConfigurer`가 이들을 엮고, 자동설정이 그것을 7곳에서 참조한다.
**`compat` (11)** — 스키마 호환성 엔진. `GraphQlSchemaComparator`(791) · `GraphQlChangeKind`(285) · `GraphQlDeprecationGate`(91) · `GraphQlCompatibilityPolicy` · `GraphQlRemovalDecision` · `GraphQlClientOwnerApproval`.
## 7. Negative-space probes — sub-scope 02
### 7.1 (8.1) 도달성 — 파일 단위 배선 전수
`autoconf` 열은 `autoconfigure` 패키지 파일들에서의 참조 수, `main_other`는 자기 파일과 `autoconfigure`를 제외한 main 참조 파일 수:
| 타입 | autoconf | main_other | test | 판정 |
|---|---:|---:|---:|---|
| `GraphQlScalarWiringConfigurer` | 7 | 0 | 2 | 배선됨 |
| `GraphQlScalarManifest` | 4 | 1 | 2 | 배선됨 |
| `GraphQlMappingInspectionGate` | 3 | 0 | 2 | 배선됨 |
| `GraphQlScalarDefinition` | 3 | 1 | 2 | 배선됨 |
| `GraphQlSchemaHash` | 3 | 2 | 2 | §8.1 |
| **`GraphQlSchemaAssembler`** | **0** | **0** | 3 | 미배선 |
| **`GraphQlSchemaContract`** | **0** | **0** | 1 | 미배선 |
| **`GraphQlOneOfSchemaGate`** | **0** | **0** | 2 | 미배선 |
| **`GraphQlOneOfInputValidator`** | **0** | **0** | 1 | 미배선 |
| 스칼라 5종(`BigDecimal`·`Date`·`Instant`·`Long`·`Uuid`) | 0 | 1~2 | 1~2 | `GraphQlScalarWiringConfigurer` 경유로 배선됨 |
### 7.2 (8.2) 조건 형제 비교 — 스키마 해시의 생산자와 소비자
`GraphQlSchemaHash`의 유일한 생산 경로는 `GraphQlSchemaAssemblyResult.schemaHash()`(`:94-95`)이고, 그 결과 타입은 `GraphQlSchemaAssembler.assemble(...)`만 만든다. 둘 다 프로덕션 호출자가 없다(§7.1).
소비 쪽은 `GraphQlPlatformActuatorEndpoint`가 생성자로 받는다. 그 클래스의 저장소 전체 참조는:
```
autoconfigure/GraphQlPlatformActuatorEndpoint.java:18 (클래스 선언)
autoconfigure/GraphQlPlatformActuatorEndpoint.java:38 (생성자)
autoconfigure/GraphQlPolicyRequestPathTest.java:28 (javadoc 언급)
autoconfigure/GraphQlPlatformStartupValidatorTest.java:108 (테스트가 직접 생성)
```
`GraphQlPlatformAutoConfiguration`의 39개 `@Bean` 중 이것을 만드는 것이 없다. §8.1.
*(이 파일은 `autoconfigure` 패키지에 있어 sub-scope 01의 분모에 포함된다. 스키마 해시 사슬의 소비 쪽이므로 여기서 함께 다룬다.)*
### 7.3 (8.3) 중복 메커니즘 — `@oneOf` 검증
`GraphQlOneOfPolicy`가 규칙을 한 곳에 두는 이유를 적는다 — "The rules are stated here once so **the schema gate and the runtime validator cannot disagree** about what the directive means." 그 두 소비자가 모두 미배선이다(§7.1).
한편 이 leaf는 `com.graphql-java:graphql-java:25.0`을 쓰고, 그 버전은 `@oneOf` 입력 객체를 스키마 빌드와 실행 양쪽에서 자체 처리한다. 즉 런타임 동작은 라이브러리가 덮고, 플랫폼 계층만 비어 있다. §8.2.
`src/main/resources``src/test/resources`의 어떤 `.graphqls`에도 `oneOf`가 없으므로 현재 스키마에서는 어느 쪽도 실행될 일이 없다.
### 7.4 (8.4) 문서/구현 드리프트
`GraphQlSchemaAssembler`의 javadoc이 막겠다고 선언한 위험:
> "Filesystem and classpath enumeration order is not stable across machines or packaging formats, and an unstable order would both **move the schema hash** and **change which of two conflicting declarations 'wins'** — so the order is imposed here rather than inherited from discovery."
프로덕션에서 SDL을 병합하는 것은 Spring GraphQL의 자체 리소스 탐색이고, 이 조립기는 실행되지 않는다. 즉 두 위험 모두 완화되지 않는다. §8.1.
`compat` 패키지는 성격이 다르다 — `GraphQlSchemaComparator`(791줄)와 `GraphQlDeprecationGate`는 릴리스 게이트에서 두 스키마를 비교하는 도구이고, `advanced/codegen`이 참조한다(`main_other=1`). 런타임 경로가 아니므로 미배선이 정상이다.
## 8. Sub-scope 02 findings
### 8.1 P2 — 스키마 조립·계약 정체성·해시 사슬이 통째로 미배선이고, 그것을 발행할 액추에이터 엔드포인트도 등록되지 않는다
사슬 전체가 끊겨 있다:
```
GraphQlSchemaAssembler.assemble(resources) 호출자 0
└─> GraphQlSchemaAssemblyResult 생산자 0
└─> .schemaHash() -> GraphQlSchemaHash 생산자 0
└─> GraphQlPlatformActuatorEndpoint(properties, schemaHash, ...) @Bean 0
└─> GraphQlPlatformConfigurationReport 발행 경로 0
GraphQlSchemaContract(hash, breakingPolicyV, scalarManifestV, directiveManifestV) 생성자 호출 0
```
**세 가지가 함께 사라진다:**
1. **결정적 병합 순서.** SDL 조각의 정렬을 조립기가 강제하도록 설계돼 있고(§7.4), 실제로는 Spring GraphQL의 탐색 순서를 그대로 쓴다. 조각이 하나(`skeleton.graphqls`)뿐인 지금은 무해하지만, adopter가 자기 `.graphqls`를 추가하는 순간 — 그것이 이 leaf의 문서화된 확장 방식이다 — 충돌 선언의 승자와 스키마 해시가 패키징 방식에 따라 달라질 수 있다.
2. **네 부분 계약 정체성.** `GraphQlSchemaContract`가 "해시만으로는 호환성을 판정할 수 없다"는 판단을 타입으로 만들었는데, 그 타입을 만드는 코드가 없다. 호환성 판정이 필요한 곳(릴리스 게이트)은 `compat`의 비교기를 직접 쓴다.
3. **운영 가시성.** `GraphQlPlatformActuatorEndpoint.report()`가 배포된 스키마 해시 · 실행 프로파일 · 배포 모드 · 활성 능력 · 등록된 연산/페치 프로파일 수를 하나의 보고서로 낸다. 등록되지 않으므로 운영자가 "이 배포가 무엇을 켜고 있는가"를 물을 표면이 없다.
**inbound-web §44.2와 같은 형태다** — 거기서는 `WebPlatformStartupValidator`(62줄)가 시작 시 실행되지 않았고, 여기서는 `GraphQlPlatformActuatorEndpoint`가 등록되지 않는다. 두 모듈 모두 fileserver 하위 트리(`attestMapping()`이 app-bootstrap에서 실제로 호출되는)와 대조된다.
**권고**`GraphQlPlatformAutoConfiguration`이 이미 39개 빈을 만들고 `GraphQlSchemaHash`를 세 곳에서 참조하므로 자리는 있다. `GraphQlSource`가 확정된 뒤 그 SDL로 `GraphQlSchemaAssembler`를 돌려 해시를 얻고, 그것으로 액추에이터 엔드포인트를 만든다. 그러면 세 가지가 함께 닫힌다.
### 8.2 P3 — `@oneOf` 게이트와 런타임 검증기가 미배선이고, "플랫폼이 강제한다"는 서술이 그것을 넘어선다
`GraphQlOneOfPolicy`의 클래스 javadoc은 "The September 2025 `@oneOf` input rules **the platform enforces**"로 시작한다. 강제하는 두 코드 — 시작 게이트와 런타임 검증기 — 는 프로덕션 호출자가 0이다.
**노출은 없다.** graphql-java 25.0이 `@oneOf`를 자체 처리하므로 런타임 거부는 라이브러리가 한다. 그리고 현재 스키마에 `@oneOf` 선언이 없다.
기록하는 것은 두 가지다: (a) 플랫폼 계층의 강제가 서술과 달리 존재하지 않는다, (b) `GraphQlOneOfSchemaGate.verify(sdl)`가 확인하는 것은 라이브러리가 확인하지 않는 부분(멤버가 전부 nullable이고 기본값이 없어야 한다는 **선언 시점** 규칙)이므로, adopter가 잘못된 `@oneOf` 입력 타입을 선언하면 시작 시점이 아니라 첫 요청에서 드러난다.
### 8.3 — `compat`·`scalar` 결함 없음
`compat` 11개 파일은 릴리스 도구이고 `advanced/codegen`과 테스트가 소비한다 — 런타임 미배선이 정상이다. `scalar` 7개는 `GraphQlScalarWiringConfigurer`(autoconf=7)를 통해 전부 배선된다.
## 9. Sub-scope 02 완료 조건
- denominator 46 / 46 FULL_READ
- §8.1~§8.4 수행 — 파일 단위 배선 전수, 해시 사슬 생산자·소비자 추적, `@oneOf` 중복 메커니즘, 조립 순서 드리프트
- P2 1건, P3 1건
- 소스 미변경
---
# Sub-scope 03 — `execution` + `context` + `runtime` (60 files, main 48 + test 12)
> 내부 상태: COMPLETE — **60 / 60 FULL_READ** · 근거 `evidence/raw/209-inbound-graphql-execution-probes.txt`
## 10. 무엇을 하는 코드인가
**`runtime` (19+1)** 이 실제 실행 사슬이고, 이 모듈에서 자동설정 참조가 가장 조밀한 곳이다. `GraphQlPlatformWebInterceptor`(167, autoconf=3) · `GraphQlExecutionChain`(82, 5) · `GraphQlPlatformInstrumentation`(103, 3) · `GraphQlPreparsedDocumentAdapter`(117, 4) · `GraphQlWireErrorMapper`(124, 4) · `GraphQlRequestBodyLimitFilter`(213, 4) · `GraphQlPrincipalResolver`(35, 4) · `GraphQlBatchLoaderRegistrar`(128, 4) · `GraphQlDataFetcherExceptionResolver`(70, 3) · `GraphQlRequestObservationConventionAdapter`(176, 3) — 전부 배선돼 있다.
**`context` (6)** 은 요청 정체성이다. `GraphQlRequestContext`(76)가 main 22개 파일에서 참조되는 이 모듈의 중심 값이고, `ActorRef` · `TenantContext` · `GraphQlDeadline` · `GraphQlIdentityFingerprinter`(150)를 담는다.
`GraphQlRequestContext.withDeadline`**조이기만 한다**:
```java
GraphQlDeadline effective = tightened.value().isBefore(deadline.value()) ? tightened : deadline;
```
들어온 값이 기존보다 늦으면 무시된다 — 하위 단계가 예산을 늘릴 수 없다.
**`execution` (22)** 은 실행 정책 어휘다. 프로파일 · 파이프라인 · 사전 파싱 캐시 · 취소 · 타임아웃 정책 · 리졸버 예산과 카탈로그 · 연산 이름 정책.
`BoundedPreparsedDocumentProvider`(194, autoconf=4)와 `GraphQlPreparsedCachePolicy`(4) · `GraphQlPreparsedCacheMetrics`(2)가 배선돼 있다 — 파싱 캐시는 GraphQL에서 무한 증가하기 쉬운 지점이고 그 상한이 실제로 걸린다.
## 11. Negative-space probes — sub-scope 03
### 11.1 (8.1) 도달성 — 배선 전수에서 남는 셋
48개 main 파일 중 `autoconf=0`이면서 자기 패키지 밖 main 참조도 0인 것은 셋이다:
| 타입 | LOC | test | 무엇을 하는가 |
|---|---:|---:|---|
| `GraphQlDeadlinePropagator` | 71 | 1 | 설계 §10의 5계층 예산 파생 |
| `GraphQlOperationNameInterceptor` | 55 | 1 | 연산 이름 정책 적용 + 컨텍스트에 연산 정체성 고정 |
| `GraphQlResolverCatalog` | 61 | 1 | 리졸버 등록부(프로파일 검사의 전제) |
### 11.2 (8.2) 조건 형제 비교 — 연산 정체성을 정하는 두 구현
`GraphQlOperationNameInterceptor.apply(...)`(미배선)와 `runtime/GraphQlOperationSelectionHandler`(autoconf=2, 배선됨)가 같은 일을 한다. **배선된 쪽이 더 많이 한다**:
```java
// GraphQlOperationSelectionHandler (:53, :65, :68, :75)
if (operations.isEmpty()) throw new GraphQlAnonymousOperationException("the document declares no operation");
if (requestedName 지정 && 일치 없음) throw ... ("the document declares no operation named " + requestedName);
if (이름 없음 && operations.size() > 1) throw ... ("operationName is required when the document declares N operations");
if (policy.namedOperationRequired() && name==null) throw ... ("this client profile requires a named operation");
...
return context.withSelection(selected, selection, context.requestContext().withOperationId(operationId(selected)));
```
익명 연산 거부 · 다중 연산 시 `operationName` 요구 · 연산 정체성 정규화가 전부 배선된 경로에 있다. 정규화 규칙에는 근거도 붙어 있다 — "any name that cannot survive normalisation becomes the anonymous identity rather than being rejected — **a naming convention is not a reason to refuse an otherwise valid request**."
따라서 미배선 인터셉터는 **누락이 아니라 중복**이다. 다만 그것이 쓰는 `GraphQlOperationNamePolicy`(85줄, 참조자 = 인터셉터와 자기 자신뿐)도 함께 미배선이고, 배선된 핸들러는 다른 정책 객체를 쓴다. §12.2.
### 11.3 (8.3) 중복 메커니즘 — 예산 계층
설계 §10이 다섯 계층을 정의하고 `GraphQlDeadlinePropagator`가 그 파생을 담는다. 실제 강제 상태:
| 계층 | 파생 지점 | 배선 |
|---|---|---|
| 전송 핸드셰이크 | — | (이 sub-scope 밖) |
| **요청** | `GraphQlPlatformWebInterceptor:135``GraphQlDeadline.after(policy.maxExecutionTime(), clock)` | **예** |
| 리졸버 | `GraphQlDeadlinePropagator.resolverBudget(...)` | 아니오 |
| DataLoader 배치 | `GraphQlDeadlinePropagator.dataLoaderBatchTimeout(...)` | 아니오 |
| 다운스트림(DB/HTTP) | `GraphQlDeadlinePropagator.downstreamDeadline(...)` | 아니오 |
| 구독 연결 | `GraphQlDeadlinePropagator.subscriptionDeadline(...)` | 아니오 |
`GraphQlTimeoutPolicy``GraphQlResolverBudget`의 main 참조자를 전수하면 전부 `execution` 패키지 안(그리고 미배선 클러스터 안)이다:
```
GraphQlTimeoutPolicy <- GraphQlRequestCancelledException, GraphQlDeadlinePropagator, GraphQlResolverBudget
GraphQlResolverBudget <- GraphQlResolverDescriptor, GraphQlResolverCatalog, GraphQlDeadlinePropagator, GraphQlExecutionProfileValidator
```
§12.1.
### 11.4 (8.4) 문서/구현 드리프트 — 취소 경로
`GraphQlCancellation`(93)은 `cost/GraphQlRuntimeBudgetTracker` · `advanced/incremental` · `advanced/subscription` 세 곳에서 쓰인다. 요청 데드라인이 실제로 실행을 끊는 경로가 존재한다는 뜻이고, `GraphQlRequestContext.withDeadline`의 단조 조이기와 함께 요청 계층은 완결돼 있다.
## 12. Sub-scope 03 findings
### 12.1 P2 — 5계층 예산 모델에서 요청 계층만 강제되고, 나머지 파생이 전부 미배선이다
`GraphQlDeadlinePropagator`의 javadoc이 계층 분리의 이유를 정확히 적는다:
> "The layers are separate because they mean different things and expire at different points: a transport handshake, the request execution, one resolver, one DataLoader batch, and — for a subscription — the connection itself. The last one is deliberately not derived from the request budget: **a subscription is a long-lived stream, and applying a five-second request timeout to it would terminate every subscription five seconds after it started.**"
그 파생을 수행하는 메서드 다섯 개 전부 프로덕션 호출자가 0이다(§11.1·§11.3).
**실제로 강제되는 것과 아닌 것:**
- 요청 전체 데드라인 — `GraphQlPlatformWebInterceptor`가 만들고 `GraphQlCancellation`이 끊는다. **작동한다.**
- 개별 리졸버가 남은 요청 예산으로 잘리는 것 — 없다.
- DataLoader 배치 타임아웃이 남은 요청 예산으로 잘리는 것 — 없다.
- DB/HTTP 다운스트림 호출에 남은 예산이 전달되는 것 — 없다.
**실패 시나리오** — 요청 예산이 5초이고 리졸버 하나가 다운스트림 HTTP를 부른다. 그 호출에 전달되는 데드라인은 아웃바운드 어댑터 자신의 기본값(예: 10초)이고, 남은 요청 예산이 1초라는 사실은 전달되지 않는다. 요청은 5초에 취소되지만 다운스트림 호출은 계속 진행되어 연결과 스레드를 4초 더 붙잡는다. `GraphQlDeadlinePropagator.downstreamDeadline`이 정확히 그 clamping을 위해 존재한다.
DataLoader 쪽은 더 직접적이다 — `dataloader/GraphQlBatchContext``GraphQlDeadline`을 레코드 컴포넌트로 갖지만, 그 값을 남은 요청 예산으로 잘라 넣는 코드가 `dataLoaderBatchTimeout`이고 호출자가 없다.
**권고**`GraphQlDeadlinePropagator`를 빈으로 등록하고 세 지점에 연결한다: `GraphQlBatchLoaderRegistrar`(autoconf=4)가 배치 타임아웃을, 리졸버 실행 경로가 `resolverBudget`을, 그리고 아웃바운드 포트 호출 지점이 `downstreamDeadline`을 쓰게 한다. 구독 계층은 §8의 `advanced/subscription`에서 별도로 확인한다.
### 12.2 P3 — 연산 이름 정책의 두 구현 중 하나만 배선되고, 미배선 쪽만 `GraphQlOperationNamePolicy`를 쓴다
§11.2. 익명 연산 거부는 배선된 `GraphQlOperationSelectionHandler`가 수행하므로 **강제 자체는 존재한다**. 기록하는 것은 정책 객체의 이원화다 — `GraphQlOperationNamePolicy`(85줄)의 참조자가 미배선 인터셉터와 자기 자신뿐이고, 배선된 핸들러는 별개의 `policy`를 쓴다. 두 정책이 "이름 있는 연산을 요구하는가"에 대해 서로 다른 답을 낼 수 있는 구조이며, 지금은 한쪽만 답한다.
### 12.3 P3/기록 — `GraphQlResolverCatalog`가 비어 있어 실행 프로파일 검사가 대상을 갖지 않는다
`GraphQlResolverCatalog`의 javadoc: "Registration is what makes the profile check possible: **an unregistered resolver cannot be checked against the runtime profile, so looking one up fails rather than assuming it is safe.**"
등록하는 코드가 없다. `GraphQlExecutionProfileValidator`(autoconf=0, main_other=1)가 `GraphQlResolverDescriptor`/`GraphQlResolverBudget`을 소비하지만 그 자신도 미배선이다. 즉 "리졸버가 실행 프로파일에 맞는가"를 판정하는 층 전체가 비어 있다.
현재 이 leaf에 리졸버가 하나(`HealthGraphqlController._health`)뿐이므로 노출은 없다. adopter가 리졸버를 기여하는 시점에 이 검사가 없다는 사실이 드러난다.
## 13. Sub-scope 03 완료 조건
- denominator 60 / 60 FULL_READ
- §8.1~§8.4 수행 — 48개 main 파일 배선 전수, 연산 정체성 두 구현 비교, 예산 5계층 강제 상태 확인, 취소 경로 확인
- P2 1건, P3 2건
- 소스 미변경
---
# Sub-scope 04 — `cost` + `policy` + `security` (57 files, main 45 + test 12)
> 내부 상태: COMPLETE — **57 / 57 FULL_READ** · 근거 `evidence/raw/210-inbound-graphql-cost-security-probes.txt`
## 14. 무엇을 하는 코드인가
GraphQL에서 비용 제어와 권한은 REST보다 어렵다 — 클라이언트가 쿼리 모양을 정하므로 "한 요청이 얼마나 비싼가"를 실행 전에 판정해야 한다. 이 세 패키지가 그 판정을 담는다.
**`cost` (21)** 은 네 계층으로 나뉜다:
| 계층 | 타입 | 배선 |
|---|---|---|
| 파서 한계(토큰 수 · 규칙 깊이) | `GraphQlParserLimits` · `GraphQlParserOptionsFactory` · `GraphQlParserLimitPolicy` | §16.1 |
| 구조 한계(깊이 · 폭 · 별칭 · 프래그먼트) | `GraphQlStructuralLimitPolicy`(autoconf=4) · `GraphQlStructuralLimits`(2) · `GraphQlDocumentShapeAnalyzer`(4, 323줄) | **예** |
| 복잡도 점수 | `GraphQlComplexityCalculator`(4) · `GraphQlCostCatalog`(4) · `GraphQlDocumentComplexityScorer`(295) · `GraphQlFieldCostDescriptor` · `GraphQlResolverWeight` | **예** |
| 런타임 예산(응답 바이트 · 노드 수) | `GraphQlRuntimeBudgetTracker`(95) · `GraphQlResponseByteLimiter` · `GraphQlResponseNodeCounter` | 간접 |
**`policy` (9)** 는 클라이언트 프로파일별 정책이다. `GraphQlClientPolicy`(125)가 이 모듈에서 가장 널리 쓰이는 정책 값이고 자동설정에서 12곳, 다른 main 12파일에서 참조된다.
**`security` (15)** 는 인증 컨텍스트 · 권한 · 테넌트 격리 · 컨텍스트 전파다.
`GraphQlContextPropagator`(88)의 논증이 특히 정확하다:
> "GraphQL execution hops threads constantly — an async data fetcher, a DataLoader dispatch, a scheduler bridge — and a context held only in a thread local silently disappears at the first hop. **That is not a lost tag: it is a batch load running with no tenant.**"
그리고 네 개 진입점(`call` · `wrap(Callable)` · `wrap(Runnable)` · `restore`) 전부 `finally`에서 이전 바인딩을 복원한다 — 풀 스레드에 남는 상태가 없다.
`GraphQlClientProfileResolver`(57)는 프로파일을 어디서 읽지 **않는지**를 먼저 말한다:
> "Never from a variable, an extension or a header the caller controls: the profile selects the cost, page-size and introspection limits, so **a caller that could name its own profile could grant itself the admin budget**."
## 15. Negative-space probes — sub-scope 04
### 15.1 (8.1) 도달성 — 배선 전수에서 남는 여섯
45개 main 파일 중 `autoconf=0`이면서 자기 패키지 밖 main 참조도 0인 것:
| 타입 | LOC | 무엇을 하는가 |
|---|---:|---|
| `GraphQlParserOptionsFactory` | 43 | graphql-java `ParserOptions`에 플랫폼 한계를 설치 |
| `GraphQlParserLimitPolicy` | 61 | 파서 한계 정책 보유 |
| `GraphQlClientPolicyManifest` | 92 | 프로파일 → 정책 매니페스트 |
| `GraphQlOperationCatalog` | 72 | 등록된 연산 목록 |
| `GraphQlPolicyViolation` | 31 | 정책 위반 값 |
| `GraphQlClientProfileResolver` | 57 | 검증된 자격 클레임 → 프로파일 |
| `GraphQlContextCleanup` | 62 | 요청 종료 시 정리 스코프 |
### 15.2 (8.2) 조건 형제 비교 — 클라이언트 정책이 어떻게 정해지는가
설계는 매니페스트 조회를 말한다:
> `GraphQlClientPolicyManifest` — "The design keeps benchmarked limits in an environment manifest rather than in application code, so **this is the one place a profile is resolved from**. An unknown profile is a startup or request failure rather than a silent fallback to a permissive default."
자동설정은 단일 빈을 만든다:
```java
// GraphQlPlatformAutoConfiguration:302-303
@Bean public GraphQlClientPolicy graphQlClientPolicy(GraphQlPlatformSettings properties) {
return GraphQlClientPolicy.defaults(...);
}
```
그리고 그 하나가 여덟 개 빈(`:123` · `:237` · `:380` · `:389` · `:397` · `:453` · `:463` …)에 주입된다. 매니페스트는 만들어지지 않는다. §16.2.
**프로파일 자체는 신뢰된 경로에서 온다**`GraphQlAuthenticationContextFactory:59``principal.clientProfile()`을 쓰고(검증된 principal), 미인증 호출자에는 `GraphQlPlatformWebInterceptor``anonymousProfile`이 붙는다. 즉 `GraphQlClientProfileResolver`가 막으려는 노출(호출자가 자기 프로파일을 지정)은 배선된 경로에서도 발생하지 않는다. 그 타입은 중복이다.
### 15.3 (8.3) 중복 메커니즘 — 컨텍스트 전파와 정리
`GraphQlContextPropagator`의 javadoc은 "**Every hop** goes through here"라고 하지만 프로덕션 사용처는 하나다 — `runtime/GraphQlBlockingBridge:80`. 나머지 홉에서 컨텍스트를 나르는 것은 graphql-java 자신의 `GraphQLContext`이고, 런타임이 그것을 읽는다(`GraphQlPreparsedDocumentAdapter:101`, `GraphQlRequestObservationConventionAdapter:128`, `GraphQlRequestContext.CONTEXT_KEY`).
**누수는 없다.** ThreadLocal을 쓰는 세 진입점 전부 `finally`에서 복원하고, 테스트가 그것을 확인한다(`GraphQlTenantIsolationPolicyTest:72,75`가 실행 후 바인딩이 비어 있음을 단언).
`GraphQlContextCleanup`(62)은 그 위에 얹는 일반 정리 스코프이고 프로덕션 등록이 없다. 전파기가 스스로 정리하므로 부재가 노출을 만들지 않는다.
### 15.4 (8.4) 문서/구현 드리프트 — 파서 한계
`GraphQlParserLimitPolicy`의 javadoc이 이 한계가 어디서 강제되는지 적는다 — "... by the parser itself through `GraphQlParserOptionsFactory`". 그 팩토리의 두 메서드:
```java
public static ParserOptions create(GraphQlParserLimits limits)
public static ParserOptions installOperationDefaults(GraphQlParserLimits limits)
```
프로덕션 호출자가 **0**이다. `GraphQlParserLimits.from(GraphQlClientPolicy policy)`(설정 → 한계 변환)도 마찬가지다. §16.1.
## 16. Sub-scope 04 findings
### 16.1 P2 — 설정으로 정한 파서 한계가 graphql-java에 설치되지 않는다
파서 계층은 GraphQL DoS 방어의 **첫 번째** 관문이다 — 복잡도 계산도 구조 분석도 문서를 파싱한 뒤에 일어나므로, 파싱 자체를 폭발시키는 문서는 그 앞에서 막아야 한다.
이 leaf는 그 한계를 값으로 갖고(`GraphQlParserLimits`, 클라이언트 정책에서 파생), 설치 함수를 갖는다(`GraphQlParserOptionsFactory.installOperationDefaults`). **호출하는 코드가 없다.**
`installOperationDefaults`라는 이름이 가리키듯 graphql-java의 파서 옵션은 정적 전역(`ParserOptions.setDefaultOperationParserOptions`)이고, 시작 시 한 번 설치하지 않으면 라이브러리 기본값이 유지된다.
**실패 시나리오** — 운영자가 `backend.graphql.limits.*`로 파서 한계를 조인다. 그 값은 `GraphQlPlatformSettings``GraphQlClientPolicy`까지 도달하지만 `GraphQlParserLimits.from(...)`을 부르는 코드가 없어 파서에 닿지 않는다. 실제로 적용되는 것은 graphql-java 25.0의 기본값이다. 설정은 받아들여지고 검증되며 효과가 없다.
**노출의 크기는 라이브러리 기본값이 정한다.** graphql-java 25.0은 토큰 수·공백 토큰 수·규칙 깊이에 자체 기본 상한을 두므로 무제한은 아니다. 그리고 구조 한계(`GraphQlDocumentShapeAnalyzer`, autoconf=4)와 복잡도 계산(autoconf=4)은 배선돼 있어 파싱 이후 계층은 작동한다. 그래서 P1이 아니라 P2다 — 침묵하는 설정 표면이자 방어 계층 하나의 부재다.
**권고**`GraphQlPlatformAutoConfiguration`에 시작 시 `GraphQlParserOptionsFactory.installOperationDefaults(GraphQlParserLimits.from(clientPolicy))`를 한 번 호출하는 초기화 지점을 둔다. 정적 전역이므로 `@Bean` 메서드보다 `InitializingBean`/`SmartInitializingSingleton`이 적절하다.
### 16.2 P2 — 프로파일별 정책 매니페스트가 미배선이라, 자격에서 해석된 프로파일이 아무 예산도 선택하지 않는다
§15.2. 클라이언트 프로파일은 검증된 principal에서 정확히 해석되고 요청 컨텍스트에 실린다. 그리고 그 값이 선택하는 것은 **캐시 키와 지표 태그뿐**이다 — 정책은 프로파일과 무관하게 단일 빈이다.
설계가 이 구조를 명시적으로 거부한다: "The design keeps benchmarked limits in an environment manifest rather than in application code." 지금은 코드 안의 `GraphQlClientPolicy.defaults(properties)` 하나다.
**실패 시나리오** — 배포가 내부 배치 클라이언트에는 큰 복잡도 예산을, 공개 모바일 클라이언트에는 작은 예산을 주려 한다. 두 프로파일이 자격에서 정확히 구분되고, 두 요청 모두 같은 `GraphQlClientPolicy`로 평가된다. 프로파일을 나눈 목적이 달성되지 않으며, 그 사실은 어떤 오류로도 드러나지 않는다 — `GraphQlClientPolicyManifest`가 약속한 "An unknown profile is a startup or request failure rather than a silent fallback to a permissive default"는 조회가 일어나지 않으므로 성립할 기회가 없다.
**권고**`GraphQlClientPolicy` 단일 빈을 `GraphQlClientPolicyManifest` 빈으로 바꾸고, 정책을 요구하는 여덟 지점이 요청 컨텍스트의 프로파일로 조회하게 한다. 매니페스트는 중복 프로파일을 생성자에서 거부하므로 설정 오류가 부팅에서 드러난다.
### 16.3 P3/기록 — 중복이거나 미사용인 네 타입
| 타입 | 판정 |
|---|---|
| `GraphQlClientProfileResolver` (57) | 중복 — 배선된 경로(`GraphQlAuthenticationContextFactory:59`)가 검증된 principal에서 프로파일을 가져오므로 보안 성질은 유지된다 |
| `GraphQlContextCleanup` (62) | 미사용 — 전파기가 `finally`로 스스로 복원하므로 부재가 누수를 만들지 않는다 |
| `GraphQlOperationCatalog` (72) · `GraphQlPolicyViolation` (31) | 미사용 — 연산 등록부는 §12.3의 `GraphQlResolverCatalog`와 같은 형태로 비어 있다 |
### 16.4 P3/기록 — `GraphQlContextPropagator`의 "every hop" 서술이 실제 사용처와 다르다
§15.3. 프로덕션 사용처가 `GraphQlBlockingBridge` 하나다. 다른 홉은 graphql-java의 `GraphQLContext`가 나르며 그것이 올바른 전송 수단이다 — 결함이 아니라 서술의 범위 문제다. 다만 "batch load running with no tenant"를 막는 것이 `GraphQLContext`라는 사실이 코드 어디에도 적혀 있지 않아, DataLoader 경로에서 테넌트가 어떻게 유지되는지는 `GraphQlBatchLoaderRegistrar`(autoconf=4)를 읽어야만 알 수 있다.
## 17. Sub-scope 04 완료 조건
- denominator 57 / 57 FULL_READ
- §8.1~§8.4 수행 — 45개 main 파일 배선 전수, 정책 결정 경로 비교, 컨텍스트 전파/정리 중복, 파서 한계 드리프트
- P2 2건, 기록 2건
- 소스 미변경
---
# Sub-scope 05 — `http` + `error` + `observation` (48 files, main 38 + test 10)
> 내부 상태: COMPLETE — **48 / 48 FULL_READ** · 근거 `evidence/raw/211-inbound-graphql-http-probes.txt`
## 18. 무엇을 하는 코드인가
**`observation` (9)** 은 이 sub-scope에서 가장 잘 배선된 부분이다. `GraphQlSensitiveAttributeFilter`(autoconf=6) · `GraphQlRequestObservationConvention`(4, 180줄) · `GraphQlMetricCardinalityPolicy`(4) · `GraphQlOperationNameCardinality`(4) · `GraphQlDataLoaderObservationConvention`(3) · `GraphQlResolverObservationConvention`(3) — 지표 태그의 카디널리티와 민감 속성 필터가 실제로 적용된다.
**`error` (10)** 은 두 갈래다. `GraphQlExceptionResolver`(autoconf=4)가 리졸버 예외를 배선된 경로에서 처리하고, `GraphQlWireError`(102)와 `GraphQlErrorContext`(40)가 각각 main 12개 파일에서 참조되는 공용 어휘다. `GraphQlInternalErrorMasker`(75)가 내부 예외를 마스킹한다.
**`http` (19)** 은 HTTP 전송 계약이다 — Accept 협상(`GraphQlAcceptHeader` 151), 미디어 타입, 요청 봉투 검증, 응답 팩토리, 상태 매퍼, 크기 한계. 설계 §9.2 · §18의 사전 파싱 한계가 여기 있고, 그 순서에 대한 논증이 정확하다:
> `GraphQlRequestEnvelopeValidator` — "Everything here runs *before* the GraphQL parser sees the document. That ordering is the point: **a parser has to allocate proportionally to its input**, so a size limit applied [later is too late]."
> `GraphQlHttpResponseFactory` — "Having one factory is what keeps the 4xx/200 split from drifting: **every transport builds its response here**, so a new outcome cannot be introduced with an ad-hoc status at one call site."
## 19. Negative-space probes — sub-scope 05
### 19.1 (8.1) 도달성 — HTTP 엔드포인트를 누가 소유하는가
```
$ grep -rn 'GraphQlHttpHandler|WebGraphQlHandler|RouterFunction|@PostMapping' src/main --include=*.java
(결과 없음)
```
이 leaf에는 **HTTP 엔드포인트를 발행하는 코드가 없다.** `/graphql`은 Spring GraphQL 자신의 자동설정이 발행하고, 그것이 §3.2의 `GraphQlOffAutoConfigurationImportFilter`가 존재하는 이유다 — "the framework serves it anyway."
따라서 `http` 패키지의 전송 기계는 소비자를 가질 수 없다. `GraphQlHttpOutcome`(main_other=8)의 참조자를 전수하면 전부 `http` 패키지 내부와 `error/GraphQlRequestErrorMapper` 하나다:
```
http/GraphQlHttpResponse · GraphQlHttpContractException · GraphQlRequestFormatException ·
GraphQlRequestTooLargeException · GraphQlHttpProfile · GraphQlHttpResponseFactory ·
GraphQlHttpStatusMapper · (자기 자신) + error/GraphQlRequestErrorMapper
```
`GraphQlRequestErrorMapper` 자신도 미배선이므로, 이 아홉 개는 닫힌 섬이다. §20.1.
### 19.2 (8.2) 조건 형제 비교 — 사전 파싱 한계의 두 구현
| | 배선 | 무엇을 하는가 |
|---|---|---|
| `runtime/GraphQlRequestBodyLimitFilter` (213) | **autoconf=4** | 요청 본문 바이트 상한 |
| `runtime/GraphQlJsonStructurePolicy` 소비 (100) | **autoconf=4** | JSON 구조 정책 |
| `http/GraphQlRequestEnvelopeValidator` (152) | 0 | 설계 §9.2·§18의 사전 파싱 한계 전체 |
| `http/GraphQlRequestSize` (88) | 0 (main_other=3, 전부 `http` 내부) | 크기 표현 |
바이트 상한과 JSON 구조는 배선된 두 컴포넌트가 덮는다. 봉투 검증기가 추가로 무엇을 확인하는지는 그 파일에만 있고, 그 차이는 프로덕션에서 실행되지 않는다.
### 19.3 (8.3) 중복 메커니즘 — 실행 전 실패의 매퍼
`GraphQlRequestErrorMapper`의 javadoc이 자기 존재 이유를 적는다:
> "A separate mapper from the resolver one, because **a `DataFetcherExceptionResolver` never sees these**: parse and validation failures happen before any data fetcher is invoked."
진단이 정확하고, 배선된 것은 리졸버 쪽(`GraphQlExceptionResolver`, autoconf=4)뿐이다. 파싱·검증 실패의 와이어 형식을 이 플랫폼이 정하지 않는다는 뜻이다 — 다만 `runtime/GraphQlWireErrorMapper`(autoconf=4)와 `runtime/GraphQlPlatformRejectionMapper`(main_other=3)가 배선돼 있어 플랫폼이 거부하는 실패(익명 연산, 복잡도 초과 등)는 안정 코드로 매핑된다. 덮이지 않는 것은 graphql-java 자신이 만드는 구문/검증 오류다.
### 19.4 (8.4) 문서/구현 드리프트 — 보고되는 HTTP 프로파일
`GraphQlPlatformConfigurationReport`(§8.1)가 `GraphQlHttpProfile.V1.name()`을 배포 상태의 일부로 보고한다. `GraphQlHttpProfile`은 autoconf=2로 참조되지만, 그 프로파일이 규정하는 전송 동작(상태 매핑 · Accept 협상 · 응답 형태)을 수행하는 코드는 미배선이다(§19.1). 그리고 그 보고서를 발행할 액추에이터 엔드포인트 자체도 등록되지 않는다(§8.1).
## 20. Sub-scope 05 findings
### 20.1 P2 — `http/`가 등급표에서 `wired`로 선언돼 있으나 그 등급의 정의를 만족하지 않는다
*(이 발견은 sub-scope 07에서 `CLAUDE.md`의 capability 등급표를 읽은 뒤 이 절로 되돌아와 다시 쓴 것이다. 등급표의 존재가 이 sub-scope의 판정을 바꾼다 — §24 참조.)*
`CLAUDE.md`가 등급을 정의한다:
| 등급 | 의미 |
|---|---|
| `modelled` | 정책·계약 객체가 있고 단위 테스트가 있다. **요청 경로에는 없다.** |
| `wired` | Spring 실행 경로에 연결돼 있고, **실제 endpoint 테스트가 그 사실을 증명한다.** |
그리고 규칙을 명시한다 — "**현재 등급보다 높게 표현하지 않는다.**"
`http/` 행은 이렇다:
```
| 요청 크기/Accept 협상 (`http/`) | `wired` | `GraphQlRequestBoundsTest`, `GraphQlAcceptNegotiationTest` |
```
**두 증거 테스트 모두 endpoint 테스트가 아니다.** `@SpringBootTest``WebEnvironment``MockMvc``ApplicationContextRunner`도 없고, 대상 타입을 직접 생성해 호출하는 순수 단위 테스트다:
```java
// GraphQlRequestBoundsTest:37-38
GraphQlRequestEnvelopeValidator validator = GraphQlRequestEnvelopeValidator.maxVariablesBytes(8);
// GraphQlAcceptNegotiationTest:102
var entries = GraphQlAcceptHeader.parse("*/*;q=0.5, application/json;q=0.5, text/html;q=0.9");
```
그리고 배선 전수가 그것과 일치한다 — `GraphQlRequestEnvelopeValidator`(152줄) autoconf=0 · main_other=0, `GraphQlAcceptHeader`(151줄) autoconf=0 · main_other=1.
**행의 두 항목을 나누어 보면:**
| 항목 | 실제 상태 |
|---|---|
| 요청 크기 | **배선됨** — 단 `http/GraphQlRequestEnvelopeValidator`가 아니라 `runtime/GraphQlRequestBodyLimitFilter`(213줄, autoconf=4)가 한다. 행이 인용한 테스트는 배선되지 않은 쪽을 시험한다 |
| Accept 협상 | **배선 안 됨**`GraphQlAcceptHeader`를 부르는 프로덕션 코드가 없다. 협상은 Spring GraphQL이 한다 |
즉 이 행은 등급표의 자기 규칙을 어기는 유일한 행이다. 다른 여섯 개 `wired` 행은 증거로 `runtime/GraphQlPlatformExecutionPathTest`(random-port) · `GraphQlBatchLoaderRegistrationTest`(실제 graphql-java 실행) · `GraphQlObservationWiringTest`(프레임워크가 실제로 해석)를 든다 — 전부 정의를 만족한다.
**왜 이것이 중요한가** — 이 등급표가 이 모듈의 주된 정직성 장치이고, 그것이 §24에서 확인하듯 실제로 작동한다(일곱 개 능력을 `modelled`로 스스로 강등하고, 그중 둘은 전용 테스트로 고정한다). 그 장치의 한 행이 틀리면, 등급표를 읽고 신뢰하는 사람이 정확히 그 행에서 틀린 결론을 얻는다.
### 20.1b 그 결과 — HTTP 전송 계약 계층이 미배선이고 실제 전송은 프레임워크가 정한다
`http` 패키지 19개 파일 중 자동설정이 값으로 소비하는 둘(`GraphQlHttpProfile` autoconf=2, `GraphQlJsonStructurePolicy` autoconf=4)을 빼면, 나머지는 실행되지 않는다:
| 미배선 | LOC | 무엇을 결정하기로 되어 있었나 |
|---|---:|---|
| `GraphQlAcceptHeader` | 151 | Accept 협상(`application/json``application/graphql-response+json`) |
| `GraphQlRequestEnvelopeValidator` | 152 | 사전 파싱 한계(설계 §9.2·§18) |
| `GraphQlHttpResponseFactory` | 84 | 4xx/200 분리 — "every transport builds its response here" |
| `GraphQlMediaTypes` | 100 | 허용 미디어 타입 |
| `GraphQlRequestSize` · `GraphQlHttpStatusMapper` · `GraphQlHttpResponse` · `GraphQlHttpResponsePolicy` · `GraphQlHttpRequestEnvelope` · `GraphQlHttpOutcome` · `GraphQlHttpExecutor` · `GraphQlExecutionOutcome` · `GraphQlExtensionsPolicy` · `GraphQlJsonValues` | 470 | 봉투·결과·확장 정책 |
GraphQL-over-HTTP에서 상태 코드 규칙은 미디어 타입에 달려 있다 — `application/json`은 실행 오류에도 200을, `application/graphql-response+json`은 실제 상태를 쓴다. 그 규칙을 `GraphQlHttpStatusMapper``GraphQlAcceptHeader`가 담고 있고, 실제로 응답을 만드는 것은 Spring GraphQL이다.
**노출이 아니라 통제권의 문제다.** Spring GraphQL 자신이 GraphQL-over-HTTP 스펙을 구현하므로 동작은 합리적이다. 잃는 것은 (a) 이 플랫폼이 선언한 프로파일(`V1`)이 실제 동작과 일치한다는 보장, (b) 사전 파싱 한계 중 봉투 검증기에만 있는 부분, (c) "새 결과 종류가 임의 상태를 갖고 한 호출 지점에 생기는 것"을 막겠다는 단일 팩토리의 목적.
**실패 시나리오** — 운영자가 `GraphQlPlatformConfigurationReport`(§8.1을 고쳐 발행하게 된 뒤)에서 `httpProfile=V1`을 읽고 그 프로파일 문서대로 클라이언트를 작성한다. 실제 응답 상태와 미디어 타입은 Spring GraphQL이 정하며, 두 문서가 다른 지점에서 클라이언트가 깨진다.
**권고** — 둘 중 하나다. (a) 프레임워크 전송을 정본으로 인정하고 `http` 패키지에서 전송 기계를 제거한 뒤 `GraphQlHttpProfile`을 프레임워크 동작의 서술로 좁힌다. (b) `WebGraphQlInterceptor`(`GraphQlPlatformWebInterceptor`가 이미 그 자리에 있다)에서 봉투 검증과 응답 정책을 적용해 프로파일을 실제로 강제한다. 지금은 선언과 실행이 분리돼 있다.
### 20.2 P3 — 파싱·검증 실패에 플랫폼 매퍼가 없다
§19.3. `GraphQlRequestErrorMapper`(57)가 그 목적으로 존재하고 미배선이다. 배선된 두 매퍼(`GraphQlExceptionResolver` · `GraphQlWireErrorMapper`)는 각각 리졸버 예외와 플랫폼 거부를 덮는다.
결과적으로 구문 오류나 검증 실패의 응답에는 이 플랫폼의 안정 `code`/`category` 확장이 붙지 않고 graphql-java의 기본 형식이 나간다. 클라이언트가 오류 코드로 분기한다면 그 분기가 파싱 오류에서만 빗나간다.
### 20.3 P3/기록 — 구독 오류 리졸버와 프로파일러 접근 정책이 미배선이다
`GraphQlSubscriptionExceptionResolver`(62)는 "the response has already been committed: there is no `data` to make partial and no status left to change"인 경우를 다룬다 — 구독은 Advanced이므로 §8에서 그 활성화와 함께 확인한다.
`GraphQlProfilerAccessPolicy`(57)는 프로파일러를 "Never as a general response extension"으로 제한한다 — 프로파일러 자체를 켜는 코드가 없으므로 현재 노출은 없다.
`GraphQlNullabilityContract`(67)는 필드별 실패 동작 선언으로, 스키마가 하나뿐인 현재는 대상이 없다.
## 21. Sub-scope 05 완료 조건
- denominator 48 / 48 FULL_READ
- §8.1~§8.4 수행 — HTTP 엔드포인트 소유자 확인, 사전 파싱 한계 두 구현 비교, 실행 전 실패 매퍼 중복, 보고되는 프로파일 드리프트
- P2 1건, P3 2건
- 소스 미변경
---
# Sub-scope 06 — `dataloader` + `fetch` + `pagination` + `mutation` (69 files, main 58 + test 11)
> 내부 상태: COMPLETE — **69 / 69 FULL_READ** · 근거 `evidence/raw/212-inbound-graphql-data-probes.txt`
## 22. 무엇을 하는 코드인가
**`dataloader` (17)** — N+1 제거. `GraphQlBatchExecutor`(121) · `GraphQlBatchResultMapper`(134) · `GraphQlBatchChunker` · `GraphQlBatchPolicyRegistry`(autoconf=4) · `GraphQlDataLoaderFactory`(autoconf=4). `runtime/GraphQlBatchLoaderRegistrar`(autoconf=4)가 Spring의 `BatchLoaderRegistry`에 붙인다. 등급표가 `wired`로 선언하고 증거로 `GraphQlBatchLoaderRegistrationTest`(실제 graphql-java 실행 + 50 parent → 3 downstream 호출)를 든다.
**`pagination` (17)** — Relay 커서 연결. `HmacGraphQlCursorCodec`(181) · `GraphQlCursorKeyRing`(81) · `GraphQlCursorPayload`(123) · `GraphQlCursorFraming`(67) · `GraphQlConnectionAssembler`(108).
**`fetch` (10)** — 선택 집합 모양에 따른 페치 프로파일 분류.
**`mutation` (14)** — 뮤테이션 계약과 멱등성. `GraphQlMutationIdempotencyInterceptor`(77) · `GraphQlMutationFingerprint`(49) · `GraphQlCanonicalInput`(87) · `GraphQlMutationContractValidator`(92).
## 23. Negative-space probes — sub-scope 06
### 23.1 (8.1) 도달성 — 네 패키지의 배선 상태
58개 main 파일 중 자동설정이 참조하는 것은 **둘**이다 — `GraphQlBatchPolicyRegistry`(4) · `GraphQlDataLoaderFactory`(4). 나머지 56개는 autoconf=0이다.
`dataloader``runtime/GraphQlBatchLoaderRegistrar`를 통해 도달하므로 배선돼 있다. `fetch`·`pagination`·`mutation` 41개 파일은 어떤 배선 경로에도 없다.
### 23.2 (8.2) 조건 형제 비교 — 커서 서명 키의 두 소비처
`backend.graphql.cursor.key-ids`를 읽는 프로덕션 코드는 둘이다:
```
autoconfigure/GraphQlPlatformStartupValidator.java:42-43
if (properties.production() && properties.cursor().keyIds().isEmpty()) {
problems.add("a cursor signing key is required; unsigned cursors are client-editable");
autoconfigure/GraphQlPlatformActuatorEndpoint.java (보고서에 포함)
```
**서명하는 코드는 없다.** `HmacGraphQlCursorCodec``GraphQlCursorKeyRing`은 autoconf=0 · main_other=0이다.
### 23.3 (8.3) 이 모듈은 그것을 이미 알고 기록해 두었다
`autoconfigure/GraphQlPolicyRequestPathTest`가 그 목적으로 존재하는 테스트 클래스이고, javadoc이 이 sub-scope의 두 사실을 정확히 서술한다:
> "Which policies are on the request path, and which only look as though they are (GQL-INT-003)."
>
> "**Cursor signing is not on the request path.** `backend.graphql.cursor.key-ids` is consumed in exactly two places — `GraphQlPlatformStartupValidator`, which refuses to start a production deployment without it, and `GraphQlPlatformActuatorEndpoint`, which reports it back. **Nothing signs a cursor with it.** ... So production demands a key identity, an operator supplies one, the endpoint confirms it is configured — and **cursors remain exactly as client-editable as they were, which is the thing the validator's own message says the key prevents.**"
>
> "**Mutation idempotency is not on the request path either.** `GraphQlMutationIdempotencyInterceptor` is referenced by no configuration."
>
> "Both are honest `modelled` capabilities by this leaf's own grading table — **the defect is not that they are unfinished, it is that a startup validator makes one of them look finished.**"
그리고 그 사실을 테스트가 고정한다 — "**this test fails the moment somebody wires one half without the other.**"
### 23.4 (8.4) 등급표와의 대조
`CLAUDE.md`의 등급표가 이 sub-scope의 네 패키지를 이렇게 매긴다:
| 영역 | 등급 | 확인 |
|---|---|---|
| DataLoader/batching (`dataloader/`) | `wired` | 일치 — `GraphQlBatchLoaderRegistrar`가 배선하고 endpoint 테스트가 증명 |
| cursor 서명 (`pagination/`) | `modelled` | 일치 — "auto-configuration이 둘 중 무엇도 생성하지 않는다" |
| mutation 멱등성 (`mutation/`) | `modelled` | 일치 |
| `fetch/` | (표에 없음) | main 소비자 0, 테스트만 |
## 24. Sub-scope 06 findings
### 24.1 P2 — 시작 검증기가 제공되지 않는 보안 성질을 요구한다
`GraphQlPlatformStartupValidator`가 프로덕션 배포를 다음 메시지로 거부한다:
> "a cursor signing key is required; **unsigned cursors are client-editable**"
그리고 그 키를 받아 서명하는 코드가 없다(§23.2). 운영자 관점의 연쇄는 이렇다:
1. 프로덕션 배포가 키 없이 시작을 거부당한다 → 키가 중요하다고 학습한다.
2. `backend.graphql.cursor.key-ids`를 설정한다 → 부팅에 성공한다.
3. 액추에이터 보고서가 그 키 식별자를 확인해 준다(그 엔드포인트를 §8.1대로 등록하면).
4. 커서는 서명되지 않은 채로 남는다 — 검증기 메시지가 막는다고 말한 바로 그 상태다.
**이것은 미구현이 아니라 잘못된 확인 신호다.** 페이지네이션이 아직 어떤 feature에도 붙지 않았으므로 지금 조작될 커서 자체가 없다. 위험은 adopter가 커서 페이지네이션을 붙이는 시점에 발생한다 — 그때 플랫폼은 이미 "키가 있으니 서명된다"는 세 가지 신호(부팅 거부 · 설정 수용 · 보고서 확인)를 준 상태다.
**이 모듈이 그것을 스스로 기록했다는 사실이 판정을 바꾸지 않는다.** `GraphQlPolicyRequestPathTest`의 javadoc이 정확히 그렇게 말한다 — "the defect is not that they are unfinished, **it is that a startup validator makes one of them look finished**." 기록은 완전하고 진단은 정확하며, 운영자에게 도달하는 신호는 여전히 세 개 다 긍정이다.
**권고** — 그 테스트가 이미 적어 둔 대로 "설계 결정이 먼저"다: `GraphQlCursorKeyRing.of``Map<String, byte[]>`를 받고 설정은 키 자체를 담지 않기로 했으므로, 키 자료의 출처(외부 시크릿 포트)를 정해야 배선할 수 있다. 그때까지의 최소 조치는 검증기 메시지를 사실에 맞추는 것이다 — 예: "a cursor signing key identity is required for the planned signed-cursor capability; cursor signing is not yet on the request path."
### 24.2 P3/기록 — `fetch`(10) · `pagination` 나머지(15) · `mutation` 나머지(13)는 adopter 대기 라이브러리다
CLAUDE.md가 이 leaf의 범위를 "feature-agnostic: 정책·계약·검증 기계만"으로 규정하고 feature 스키마·컨트롤러·리졸버는 sample 모듈이 소유한다고 명시한다. `fetch` 프로파일 분류, 커서 조립, 뮤테이션 계약 검증은 전부 리졸버가 있어야 호출되는 것들이다.
grpc 모듈(§3.1)과 같은 형태이고, 문서와 기본값과 테스트가 일치한다. 결함이 아니다. 다만 `fetch/`는 등급표에 행이 없어, 일곱 개 `modelled` 항목과 달리 등급이 선언되지 않은 유일한 Stable 패키지다.
### 24.3 — `dataloader` 결함 없음
17개 파일이 `GraphQlBatchLoaderRegistrar`를 통해 배선되고, 배치 정책 레지스트리와 팩토리가 자동설정에 등록되며, 실제 graphql-java 실행으로 N+1 제거가 증명된다(50 parent → 3 downstream). `GraphQlBatchErrorPolicy` · `GraphQlMissingKeyPolicy` · `GraphQlBatchTimeoutException`이 배치 실패의 세 종류를 구분한다.
## 25. Sub-scope 06 완료 조건
- denominator 69 / 69 FULL_READ
- §8.1~§8.4 수행 — 58개 main 파일 배선 전수, 커서 키 두 소비처 추적, 모듈 자체 기록과 대조, 등급표 대조
- P2 1건, 기록 1건
- 소스 미변경
---
# Sub-scope 07 — `release` (10 files, main 9 + test 1)
> 내부 상태: COMPLETE — **10 / 10 FULL_READ** · 근거 `evidence/raw/213-inbound-graphql-release-probes.txt`
## 26. 무엇을 하는 코드인가
릴리스 게이트와 능력 매니페스트. 이 sub-scope는 파일 수로는 가장 작지만, **이 모듈의 정직성 장치가 어디에 있고 어디까지 작동하는지**를 결정한다.
`GraphQlReleaseGate`(78)가 다섯 종류의 증거(schema · contract · performance · fault · compatibility)를 요구하고, 없으면 이름을 붙여 거부한다. `GraphQlReleaseOverride`(48)가 예외를 허용하되 그것도 명시적 기록을 요구한다.
`GraphQlStableCapabilityManifest`(91)가 능력을 네 등급으로 나눈다 — `STABLE` 13개 · `ADVANCED` 6개 · `EXPERIMENTAL` 3개 · `UNSUPPORTED` 11개. `UNSUPPORTED` 항목마다 대안이 지정돼 있고("uploads go through the Fileserver, atomic multi-step work goes through one mutation use case, and cross-request caching goes through the cache capability"), 그중 하나에는 강등 이력이 주석으로 남아 있다:
> `SPRING_DATA_REPOSITORY_AUTO_EXPOSURE` — "Was an allowlisted Advanced capability. **An allowlist that lets a repository back a GraphQL field is still a controller reaching a repository** — the second canonical hard-stop in AGENTS.md — and **a capability flag cannot make an architectural rule conditional.** Resolvers reach storage through an application use case or not at all."
## 27. 이 모듈의 정직성 장치 — 그리고 그것이 이 분석에 미친 영향
`CLAUDE.md`가 네 등급을 정의하고 규칙을 선언한다:
| 등급 | 의미 |
|---|---|
| `modelled` | 정책·계약 객체가 있고 단위 테스트가 있다. 요청 경로에는 없다. |
| `wired` | Spring 실행 경로에 연결돼 있고, 실제 endpoint 테스트가 그 사실을 증명한다. |
| `integration-verified` | 실제 외부 시스템과의 통합 증거가 있다. |
| `production-verified` | 실부하·장애 시나리오 증거가 있다. |
> "capability 마다 아래 등급을 쓰고, **현재 등급보다 높게 표현하지 않는다.**"
그리고 13개 행 중 **일곱을 스스로 `modelled`로 강등한다** — object 인가 · cursor 서명 · mutation 멱등성 · persisted operation · subscription/WebSocket/SSE/RSocket · federation/incremental/codegen/compat. 그중 둘(cursor 서명 · mutation 멱등성)은 전용 테스트 `GraphQlPolicyRequestPathTest`가 "누군가 한쪽만 배선하는 순간 실패"하도록 고정한다.
**이것은 이 저장소에서 가장 강한 자기 공시다.** 앞선 두 모듈과 대조하면:
| | inbound-web | inbound-graphql |
|---|---|---|
| 미배선 계층의 규모 | main 397 중 다수 | main 411 중 다수 |
| 그 사실의 문서화 | 없음 — README는 반대로 서술(§8.1) | 등급표 13행 + 전용 테스트 + 티켓 ID(GQL-INT-003) |
| 검증 장치가 그것을 가리는가 | 예 — 픽스처가 조립하고 레인이 픽스처를 인증(§48.1) | 아니오 — `modelled`인 것을 `wired`라고 주장하지 않는다 |
따라서 이 모듈에 대한 분석의 초점은 "무엇이 미배선인가"가 아니라 **"등급표가 실제 배선과 일치하는가"**가 된다. 미배선이면서 등급표가 그것을 인정하는 항목은 결함이 아니고, 등급표가 인정하지 않는 항목만 결함이다.
## 28. Negative-space probes — sub-scope 07
### 28.1 (8.4) 등급표 13행 대 배선 전수 — 전수 대조
sub-scope 02~06의 파일 단위 배선 데이터와 등급표를 맞춘 결과:
| 등급표 행 | 선언 | 관측 | 판정 |
|---|---|---|---|
| 실행 파이프라인 / 인가 / cost 예산 | `wired` | `GraphQlExecutionChain`(autoconf=5) · `GraphQlAuthorizationPolicy`(5) · `GraphQlCostBudgetHandler`(2) | 일치 |
| depth/complexity 제한 | `wired` | `GraphQlStructuralLimitPolicy`(4) · `GraphQlComplexityCalculator`(4) · `GraphQlDocumentShapeAnalyzer`(4) | 일치 |
| preparsed document cache | `wired` | `BoundedPreparsedDocumentProvider`(4) · `GraphQlPreparsedDocumentAdapter`(4) | 일치 |
| 커스텀 scalar | `wired` | `GraphQlScalarWiringConfigurer`(7) | 일치 |
| **요청 크기/Accept 협상 (`http/`)** | **`wired`** | `GraphQlRequestEnvelopeValidator` 0·0 · `GraphQlAcceptHeader` 0·1, 인용된 두 테스트 모두 단위 테스트 | **불일치 → §20.1** |
| 관측 tag cardinality | `wired` | `GraphQlSensitiveAttributeFilter`(6) · `GraphQlRequestObservationConvention`(4) | 일치 |
| object 인가 | `modelled` | `ApplicationObjectAuthorization` main_other=1 | 일치 |
| DataLoader/batching | `wired` | `GraphQlBatchPolicyRegistry`(4) · `GraphQlDataLoaderFactory`(4) + `GraphQlBatchLoaderRegistrar`(4) | 일치 |
| cursor 서명 | `modelled` | `HmacGraphQlCursorCodec` 0·0 | 일치(§24.1은 등급이 아니라 검증기 메시지에 대한 것) |
| mutation 멱등성 | `modelled` | `GraphQlMutationIdempotencyInterceptor` 0·0 | 일치 |
| persisted operation | `modelled` | §9에서 확인 | — |
| subscription / WS / SSE / RSocket | `modelled` | §8에서 확인 | — |
| federation / incremental / codegen / compat | `modelled` | §10에서 확인 | — |
**13행 중 12행이 일치하고 한 행이 어긋난다.**
### 28.2 (8.2) 조건 형제 비교 — 두 능력 목록이 커서에 대해 다르게 답한다
`GraphQlStableCapabilityManifest.STABLE``SIGNED_CURSOR_CONNECTION`이 들어 있다. `CLAUDE.md` 등급표는 cursor 서명을 `modelled`(요청 경로에 없음)로 매긴다.
두 목록의 용도가 다르다 — 매니페스트는 `requireStable(capability)`로 **릴리스 게이트가 소비하는 기계 판정**이고, 등급표는 사람이 읽는 공시다. 그러나 같은 능력에 대해 하나는 "Stable에서 지원"이라 하고 하나는 "요청 경로에 없음"이라 한다. §29.2.
### 28.3 (8.1) 도달성 — 릴리스 게이트 자체
`release` 9개 파일 전부 autoconf=0이다. 릴리스 게이트는 런타임 컴포넌트가 아니라 빌드·릴리스 시점 도구이므로 정상이다. 다만 `GraphQlReleaseReportWriter`(57)는 main_other=0 · test=1로, 게이트 결과를 기록할 작성기에 호출자가 없다.
CLAUDE.md가 그 상태를 명시한다 — "`graphqlPerformanceTest` 레인이 자리를 예약, **증거 없으면 릴리스 게이트가 거부**". 즉 게이트는 CI 레인에서 호출되도록 설계됐다.
### 28.4 (8.3) 중복 메커니즘 — 없음
능력 등급은 매니페스트(기계)와 CLAUDE.md 표(사람) 두 곳에 있고 그것은 의도된 이중화다. 문제는 중복이 아니라 §28.2의 불일치다.
## 29. Sub-scope 07 findings
### 29.1 P2 — `http/` 행이 등급표의 자기 규칙을 어긴다 (§20.1 참조)
§28.1. 13행 중 유일하게 관측과 어긋나는 행이고, 어긋나는 방향이 **높게** 표현하는 쪽이다 — 등급표가 명시적으로 금지한 방향이다("현재 등급보다 높게 표현하지 않는다"). 상세와 권고는 §20.1.
### 29.2 P3 — 기계가 읽는 능력 매니페스트와 사람이 읽는 등급표가 커서 서명에 대해 다르게 답한다
`GraphQlStableCapabilityManifest.STABLE``SIGNED_CURSOR_CONNECTION`을 지원 목록에 넣는다. 그 목록의 용도는 `requireStable(...)`이고, javadoc은 이렇게 말한다:
> "Verifies a capability may be activated on the Stable starter. @throws GraphQlReleaseException when it is Advanced, Experimental or unsupported"
즉 이 매니페스트는 "Stable 스타터에서 켜도 되는가"를 판정한다. cursor 서명은 켤 수 있는 것으로 판정되고, 켜는 코드는 없다.
**실패 시나리오** — adopter가 릴리스 게이트를 돌려 `SIGNED_CURSOR_CONNECTION`이 Stable에서 승인되는 것을 확인하고, 그것을 근거로 커서 페이지네이션을 Stable 계약의 일부로 문서화한다. §24.1의 세 긍정 신호(부팅 거부 · 설정 수용 · 액추에이터 보고)에 네 번째가 더해진다.
**권고** — 매니페스트에 `MODELLED` 집합을 추가하거나, `SIGNED_CURSOR_CONNECTION``EXPERIMENTAL`로 옮긴다. 등급표와 매니페스트가 같은 사실을 말하도록 두 목록을 한 테스트로 묶는 것이 더 낫다 — 이 leaf는 이미 `WebArchitectureRulesTest` 형태의 개수 고정을 두 번 쓰고 있다(§3.4).
### 29.3 P3/기록 — `GraphQlReleaseReportWriter`에 호출자가 없다
§28.3. 게이트(`verify`)와 증거 모델(`GraphQlReleaseEvidence`)은 서로를 참조하지만 결과를 기록하는 작성기는 어디서도 불리지 않는다. 릴리스 레인이 아직 이 leaf 밖에 있으므로(“증거 자체는 실제 부하 인프라를 요구하므로 이 leaf 밖에서 생성한다”) 현재로서는 정합적이지만, 게이트를 부르는 코드가 저장소에 없다는 사실은 함께 기록해 둔다.
## 30. Sub-scope 07 완료 조건
- denominator 10 / 10 FULL_READ
- §8.1~§8.4 수행 — 등급표 13행 전수 대조, 두 능력 목록 비교, 릴리스 게이트 도달성
- P2 1건(§20.1과 동일 사안), P3 1건, 기록 1건
- 소스 미변경
---
# Sub-scope 08 — `advanced/` 스트리밍 (`subscription`·`websocket`·`sse`·`incremental`·`rsocket`) (51 files, main 45 + test 6)
> 내부 상태: COMPLETE — **51 / 51 FULL_READ** · 근거 `evidence/raw/214-inbound-graphql-advanced-streaming-probes.txt`
## 31. 관측과 등급의 대조
45개 main 파일 **전부 autoconf=0**이다. 등급표의 선언과 정확히 일치한다:
> subscription / WebSocket / SSE / RSocket | `modelled` | 정책·상태기계 단위 테스트만. **Spring transport handler 는 없다(그래서 타입 이름도 `*Admission` 이다)**
괄호 안의 문장이 이 sub-scope에서 가장 값이 크다 — **타입 이름이 등급을 인코딩한다.** `GraphQlWebSocketAdmission`(71) · `GraphQlSseAdmission`(63) · `GraphQlRSocketAdmission`(55)은 "허용 판정"이지 핸들러가 아니다.
`*HandlerFactory` 세 개(281 LOC)도 이름과 달리 핸들러를 만들지 않는다. 각각의 javadoc이 "**Decides whether** the ... handler may exist, and on what terms"로 시작하고, 팩토리인 이유를 세 입력으로 설명한다:
> `GraphQlWebSocketHandlerFactory` — "A factory rather than a bean definition because the decision has three inputs and only one of them is a flag: the capability must be enabled, the properties must describe a connection that can actually be bounded, and the admission policy must be present. A configuration class that checked only the flag would produce a handler with an unbounded connection lifetime whenever a deployment forgot the rest, and **an unbounded WebSocket is a connection slot held by whoever opens it.**"
`GraphQlSseHandlerFactory`는 SSE 변형의 성질을 미리 밝힌다 — "one POST per subscription, each holding its own connection... **a client with twenty subscriptions holds twenty connections and a browser's six-per-origin limit is** [the surprise]."
`GraphQlRSocketHandlerFactory`는 실험 등급에 게이트를 하나 더 건다 — "A flag can be set by anyone editing configuration; **the approval is a separate act**, and separating them is what stops an experimental transport from being switched on the way a supported one would be."
## 32. Findings — 없음
45개 파일이 선언된 등급(`modelled`)과 일치하고, 상태기계·정책·종료 조건이 단위 테스트로 덮여 있다. 이름 규약(`*Admission`)이 등급을 코드에서 읽을 수 있게 만든다. 이 sub-scope에는 등급표와 어긋나는 항목이 없다.
§20.3에서 기록한 `GraphQlSubscriptionExceptionResolver`(미배선)도 이 등급 안에 있다.
## 33. 완료 조건 — denominator 51 / 51 FULL_READ · 소스 미변경
---
# Sub-scope 09 — `advanced/` 요청 성형 (`persisted`·`get`·`replay`·`chaining`·`admin`) (53 files, main 46 + test 7)
> 내부 상태: COMPLETE — **53 / 53 FULL_READ** · 근거 `evidence/raw/215-inbound-graphql-advanced-shaping-probes.txt`
## 34. 관측과 등급의 대조
46개 main 파일 전부 autoconf=0. 등급표: "persisted operation (`advanced/persisted/`) | `modelled` | 중립 `OperationalRecordStorePort` 기반 레지스트리 + 방향성 테스트. **durable 구현체는 미제공**".
`persisted`(15)의 의존 방향이 특히 잘 잡혀 있다. CLAUDE.md가 그 이유를 적는다:
> "이 leaf 는 중립 계약 `dev.caskeleton.shared.opstore.OperationalRecordStorePort` 에만 의존하고 key/value 매핑만 소유한다. Postgres/Redis 구현체는 **그 중립 계약을** 구현하며, 이 leaf 의 타입을 구현하지 않는다(**그랬다면 인프라 → 인바운드 전송으로 의존이 뒤집힌다**)."
`GraphQlPersistedOperationId`(main_other=10) · `GraphQlPersistedOperation`(9) · `GraphQlPersistedOperationTransition`(80) · `GraphQlPersistedOperationStatus`가 등록·승인·폐기 상태기계를 이룬다. `advanced/admin`(10)이 그 위의 관리 연산이고, testFixtures가 인메모리 구현 둘(`InMemoryGraphQlPersistedOperationRegistry` · `InMemoryGraphQlPersistedOperationAdminPort`)을 제공한다.
`advanced/get`(7)은 HTTP GET 초안 프로파일, `advanced/replay`(8)는 구독 재생 위치, `advanced/chaining`(6)은 DataLoader 체이닝이다 — 셋 다 `EXPERIMENTAL` 또는 `ADVANCED` 등급.
## 35. Findings — 없음
선언된 등급과 관측이 일치하고, 의존 방향이 명시적으로 논증돼 있으며, 상태기계가 테스트로 덮여 있다.
## 36. 완료 조건 — denominator 53 / 53 FULL_READ · 소스 미변경
---
# Sub-scope 10 — `advanced/` 스키마·플랫폼 (`federation`·`composition`·`codegen`·`springdata`·`security`·`release`·`bootstrap`) (59 files, main 50 + test 9)
> 내부 상태: COMPLETE — **59 / 59 FULL_READ** · 근거 `evidence/raw/216-inbound-graphql-advanced-platform-probes.txt`
## 37. 무엇을 하는 코드인가
**`advanced/bootstrap` (6)** 이 Advanced 활성화 모델이다. `GraphQlAdvancedCapability`(59, main_other=15) · `GraphQlAdvancedFeatureFlags`(40) · `GraphQlAdvancedModuleGuard`(63) · `GraphQlAdvancedCapabilityDisabledException`.
`GraphQlAdvancedFeatureFlags`의 첫 문장이 이 분리의 이유다:
> "Nothing is on by default. **An Advanced capability that arrived because a dependency was added is exactly what the Stable/Advanced split exists to prevent.**"
**`advanced/springdata` (7)** 은 강등의 흔적이다. `GraphQlStableCapabilityManifest.UNSUPPORTED``SPRING_DATA_REPOSITORY_AUTO_EXPOSURE`가 있고 그 옆에 이유가 적혀 있다 — "Was an allowlisted Advanced capability. An allowlist that lets a repository back a GraphQL field is still a controller reaching a repository — the second canonical hard-stop in AGENTS.md — and **a capability flag cannot make an architectural rule conditional.**"
**`advanced/composition`(8) · `federation`(7) · `codegen`(8) · `release`(7) · `security`(7)** 은 전부 `modelled`.
## 38. Negative-space probes
### 38.1 (8.1) 도달성 — Stable 자동설정이 Advanced를 건드리지 않는가
50개 main 파일 전부 autoconf=0이다. 그리고 그것은 의도된 것이다 — `GraphQlPlatformStartupValidator`의 거부 규칙 중 하나가 "the Stable starter must not activate Advanced capabilities"다. Stable 루트가 Advanced를 참조하지 않는 상태가 그 규칙의 구조적 형태다.
`AutoConfiguration.imports`에는 항목이 하나(`GraphQlRootAutoConfiguration`)뿐이므로 **Advanced 진입점은 존재하지 않는다.**
### 38.2 (8.4) 문서/구현 드리프트 — "기본 비활성"이라는 서술
CLAUDE.md:180:
> "Advanced capability 는 전부 **기본 비활성**이다(`advanced/bootstrap/GraphQlAdvancedFeatureFlags`). EXPERIMENTAL 등급(RSocket, incremental delivery, HTTP GET draft)은 명시적 승인 없이는 `GraphQlAdvancedModuleGuard` 가 production 활성화를 거부한다."
`GraphQlAdvancedFeatureFlags``@ConfigurationProperties`가 아니라 정적 팩토리(`disabled()` · `enabling(...)` · `withExperimentalApproval()`)만 가진 record다. `backend.graphql.advanced.*` 같은 프로퍼티 접두사가 저장소 어디에도 없고, Advanced 자동설정도 없다. §39.1.
## 39. Findings
### 39.1 P3 — "기본 비활성"은 존재하지 않는 스위치의 기본값을 서술한다
§38.2. Advanced 능력을 켤 설정 표면이 없다 — 플래그 record는 코드에서만 만들어지고(`enabling(...)`은 테스트가 부른다), 그것을 바인딩하거나 소비하는 자동설정이 없다.
**등급표는 이 상태를 정확히 말한다** — Advanced 항목이 전부 `modelled`("요청 경로에는 없다")이므로, 능력이 동작한다고 주장하지 않는다. 어긋나는 것은 CLAUDE.md 산문의 활성화 서술뿐이다: "기본 비활성"과 "명시적 승인 없이는 production 활성화를 거부한다"는 둘 다 활성화 경로의 존재를 전제한다.
inbound-web §36.1과 같은 형태이되 **심각도가 다르다.** 거기서는 11개 능력이 선언되고 2개만 켤 수 있으면서 그 사실이 어디에도 없었다. 여기서는 켤 수 없다는 사실이 등급표에 `modelled`로 적혀 있고, 산문 한 문단만 그보다 앞서 나간다.
**권고** — 그 문단을 등급에 맞춘다: "Advanced capability 는 현재 `modelled` 등급이며 활성화 경로가 없다. `GraphQlAdvancedFeatureFlags`·`GraphQlAdvancedModuleGuard`는 그 경로가 생길 때 쓸 판정 모델이다."
### 39.2 — 그 외 결함 없음
`advanced/springdata`의 강등 기록, Stable 루트가 Advanced를 참조하지 않는 구조, `GraphQlAdvancedModuleGuard`의 두 단계(플래그 + 실험 승인) 모두 선언과 일치한다.
## 40. 완료 조건 — denominator 59 / 59 FULL_READ · P3 1건 · 소스 미변경
---
# Sub-scope 11 — `testFixtures` + test 잔여 (21 files, testFixtures 16 + test 5)
> 내부 상태: COMPLETE — **21 / 21 FULL_READ** · 근거 `evidence/raw/217-inbound-graphql-testkit-probes.txt`
## 41. 무엇을 하는 코드인가
`testFixtures`(16)가 계약 스위트와 통합 픽스처를 담는다 — `GraphQlSchemaContractSuite` · `GraphQlSecurityContractSuite` · `GraphQlHttpContractSuite` · `GraphQlPaginationContractSuite` · `GraphQlDataLoaderContractSuite` 다섯 개 계약 스위트와, `GraphQlJpaIntegrationFixture` · `GraphQlMongoIntegrationFixture` · `GraphQlStorageIntegrationEvidence` 세 개 저장소 통합 계약, 그리고 `GraphQlDownstreamFailureFixture` · `GraphQlPartialResponseFixture` · `GraphQlContractViolation` · `GraphQlRequestContexts`.
`advanced` 아래 인메모리 구현 둘(`InMemoryGraphQlPersistedOperationRegistry` · `InMemoryGraphQlPersistedOperationAdminPort`)이 `modelled` persisted operation의 테스트용 저장소다.
## 42. Negative-space probes
### 42.1 (8.1) 도달성 — 통합 증거 계약의 위치
CLAUDE.md가 이 소스셋의 경계를 명시한다:
> "실제 datastore 통합 증거 — `testkit/GraphQlJpaIntegrationFixture` / `GraphQlMongoIntegrationFixture` 가 계약을 정의하고 `GraphQlStorageIntegrationEvidence` 가 증거를 요구한다. **실 datastore 기동은 persistence leaf 의 책임 범위다.**"
즉 이 픽스처들은 계약만 정의하고 실행은 다른 leaf가 한다 — `testFixtures` 소스셋으로 발행하는 이유다.
### 42.2 (8.3) 중복 메커니즘 — 계약 스위트와 이 leaf의 테스트
다섯 개 계약 스위트는 adopter가 자기 스키마·리졸버에 대해 돌릴 수 있도록 만들어졌고, 이 leaf 자신의 103개 테스트와 목적이 다르다(전자는 채택자용 계약, 후자는 이 leaf의 구현). 중복 아님.
## 43. Findings — 없음
## 44. 완료 조건 — denominator 21 / 21 FULL_READ · 소스 미변경
---
# 45. 모듈 종합 — `adapter-inbound-graphql`
## 45.1 커버리지 원장 정산
| # | sub-scope | main | test | 기타 | 합 |
|---|---|---:|---:|---:|---:|
| 1 | governance + `autoconfigure` + `moduleboundary` + `architecture` + `api` | 35 | 21 | 4 | 60 |
| 2 | `schema` + `scalar` + `compat` | 37 | 9 | | 46 |
| 3 | `execution` + `context` + `runtime` | 48 | 12 | | 60 |
| 4 | `cost` + `policy` + `security` | 45 | 12 | | 57 |
| 5 | `http` + `error` + `observation` | 38 | 10 | | 48 |
| 6 | `dataloader` + `fetch` + `pagination` + `mutation` | 58 | 11 | | 69 |
| 7 | `release` | 9 | 1 | | 10 |
| 8 | `advanced/` 스트리밍 | 45 | 6 | | 51 |
| 9 | `advanced/` 요청 성형 | 46 | 7 | | 53 |
| 10 | `advanced/` 스키마·플랫폼 | 50 | 9 | | 59 |
| 11 | `testFixtures` + test 잔여 | 0 | 5 | 16 | 21 |
| | **합계** | **411** | **103** | **20** | **534** |
`FULL_READ 534 · STRUCTURAL_ONLY 0 · EXCLUDED 0 · UNCLASSIFIED 0`. 각 sub-scope의 분모는 evidence `207``217``OWNED FILES` 블록이 확인한다.
## 45.2 발견 종합 — P1 0건 · P2 5건 · P3 6건 · 기록 3건
| 심각도 | § | 발견 |
|---|---|---|
| P2 | 8.1 | 스키마 조립·계약 정체성·해시 사슬이 통째로 미배선이고, 그것을 발행할 액추에이터 엔드포인트(`GraphQlPlatformActuatorEndpoint`)도 `@Bean`이 없다 |
| P2 | 12.1 | 설계 §10의 5계층 예산 모델에서 요청 계층만 강제되고 리졸버·DataLoader 배치·다운스트림 파생이 전부 미배선(`GraphQlDeadlinePropagator` 호출자 0) |
| P2 | 16.1 | 설정으로 정한 파서 한계가 graphql-java에 설치되지 않는다(`GraphQlParserOptionsFactory` 호출자 0) — `backend.graphql.limits.*`가 파서에 닿지 않는다 |
| P2 | 16.2 | 프로파일별 정책 매니페스트가 미배선이라, 자격에서 정확히 해석된 클라이언트 프로파일이 캐시 키와 지표 태그만 고르고 예산은 고르지 않는다 |
| P2 | 20.1 / 29.1 | **등급표의 `http/` 행이 `wired`로 선언돼 있으나 인용된 두 증거 테스트가 endpoint 테스트가 아니고 대상 타입은 미배선** — 등급표 13행 중 유일하게 관측과 어긋나는 행이며, 어긋나는 방향이 표가 금지한 "높게 표현" 쪽 |
| P2 | 24.1 | 시작 검증기가 "unsigned cursors are client-editable"로 프로덕션을 거부하며 커서 서명 키를 요구하고, 그 키로 서명하는 코드가 없다 — 부팅 거부·설정 수용·액추에이터 확인 세 신호가 전부 긍정 |
| P3 | 8.2 · 12.2 · 12.3 · 16.3 · 20.2 · 29.2 · 39.1 | `@oneOf` 게이트/검증기 미배선 · 연산 이름 정책 이원화 · 리졸버 카탈로그 공백 · 중복 4타입 · 파싱/검증 실패 매퍼 부재 · 기계 매니페스트와 등급표가 커서에 대해 다른 답 · "기본 비활성"이 없는 스위치를 서술 |
## 45.3 이 모듈의 성격 — 자기 공시가 작동하는 첫 사례
**P1이 없다.** 그리고 그 이유가 이 모듈의 핵심이다.
이 leaf도 앞선 두 모듈과 같은 병을 갖는다 — main 411 파일 중 프로덕션 요청 경로에 있는 것은 소수이고, 정책·계약 객체 다수가 단위 테스트만 갖는다. 차이는 **그 사실을 스스로 등급으로 말한다**는 점이다.
```
| 등급 | 의미 |
| modelled | 정책·계약 객체가 있고 단위 테스트가 있다. 요청 경로에는 없다. |
| wired | Spring 실행 경로에 연결돼 있고, 실제 endpoint 테스트가 그 사실을 증명한다. |
...
"capability 마다 아래 등급을 쓰고, 현재 등급보다 높게 표현하지 않는다."
```
13개 행 중 **일곱을 스스로 `modelled`로 강등**하고, 그중 둘(cursor 서명 · mutation 멱등성)은 전용 테스트 `GraphQlPolicyRequestPathTest`가 "누군가 한쪽만 배선하는 순간 실패"하도록 고정한다. 그 테스트의 javadoc은 이 분석이 도달했을 결론을 먼저 적어 두었다:
> "Which policies are on the request path, and which only look as though they are (GQL-INT-003). ... **the defect is not that they are unfinished, it is that a startup validator makes one of them look finished.**"
그리고 타입 이름까지 등급을 인코딩한다 — 스트리밍 전송이 `*Handler`가 아니라 `*Admission`인 이유가 등급표에 명시돼 있다.
**세 모듈의 대조:**
| | inbound-web (14) | inbound-grpc (15) | inbound-graphql (16) |
|---|---|---|---|
| main 파일 | 400 | 8 | 411 |
| 조립 진입점 | 자동설정 2 + 컴포넌트 스캔 + app-bootstrap 19타입 | `@Configuration` 1 | 자동설정 1(루트) + import 필터 1 + 환경 후처리기 1 |
| 미배선 계층 | 다수 | 없음 | 다수 |
| 그 사실의 공시 | **없음** — README가 반대로 서술 | 해당 없음 | **등급표 13행 + 전용 테스트 + 티켓 ID** |
| P1 | 6 | 0 | **0** |
**규모가 아니라 공시가 P1을 만들거나 없앤다.** inbound-web과 graphql은 미배선 규모가 비슷하고, web은 P1 여섯 건, graphql은 0건이다. 차이는 web의 README가 실제와 반대되는 계약을 서술하고 다섯 레인이 픽스처의 조립을 인증한 반면, graphql은 미배선을 등급으로 선언하고 그 선언을 테스트로 고정했다는 점이다.
**그래서 이 모듈의 P2 다섯 건 중 가장 무거운 것은 §20.1이다** — 등급표의 한 행이 틀렸다는 것. 그 표가 이 모듈의 주된 정직성 장치이므로, 표를 읽고 신뢰하는 사람은 정확히 그 행에서 틀린 결론을 얻는다. 나머지 네 건(§8.1 · §12.1 · §16.1 · §16.2)은 등급표에 행이 없는 영역이다 — 스키마 해시 사슬, 예산 계층, 파서 한계, 정책 매니페스트. 표가 덮지 않는 곳이 결함이 모이는 곳이라는 관찰이 이 모듈의 결론이다.
## 45.4 실행 검증
```
$ ./gradlew :adapter:inbound:graphql:test
> Task :adapter:inbound:graphql:test
BUILD SUCCESSFUL in 15s
GRADLE_EXIT=0
test-results 집계: classes=186 tests=1603 failures=0 errors=0 **skipped=0**
```
CLAUDE.md가 세 개 플랫폼 레인을 더 문서화한다 — `graphqlStableTest`(605) · `graphqlContractTest`(9) · `graphqlAdvancedTest`(152). 기본 `test``quarantine`·`graphql-performance` 태그를 제외한다. 이 분석은 기본 레인만 실행했다.
## 45.5 완료 게이트
- [x] denominator 534 / 534 FULL_READ, `STRUCTURAL_ONLY 0 · EXCLUDED 0 · UNCLASSIFIED 0`
- [x] 11개 sub-scope 각각 §8.1~§8.4 negative-space probe 수행 — 411개 main 파일 전부에 대해 (autoconf 참조 / 자기·autoconfigure 제외 main 참조 / test 참조) 삼중 카운트를 기계 산출
- [x] evidence `205``217` 생성 (패키지 도달성 지도 `206` 포함)
- [x] 실행 검증: `:adapter:inbound:graphql:test``BUILD SUCCESSFUL`, tests=1603 failures=0 **skipped=0**
- [x] 거짓 양성 후보 검증 후 기각: `GraphQlContextPropagator`의 ThreadLocal 누수 의심(→ 네 진입점 전부 `finally` 복원, 테스트가 확인) · `GraphQlOperationNameInterceptor` 미배선이 익명 연산 허용으로 이어진다는 의심(→ 배선된 `GraphQlOperationSelectionHandler`가 네 가지 거부를 모두 수행) · `@oneOf` 런타임 미검증 의심(→ graphql-java 25.0이 자체 처리) · `advanced/*` 전면 미배선이 결함이라는 의심(→ 등급표가 `modelled`로 선언)
- [x] **분석 중 판정 변경**: sub-scope 05를 처음 "HTTP 전송 계층 미배선"으로만 기록했으나, sub-scope 07에서 `CLAUDE.md` 등급표를 읽은 뒤 그 행이 `wired`로 선언돼 있다는 사실이 드러나 §20.1을 등급표 위반으로 다시 썼다. 근거 `evidence/raw/211`·`213`
- [x] 소스 미변경
## Source anchors
이 문서가 backtick으로 인용한 타입·경로를 저장소 트리에 대고 해석한 결과다. 해석된 것만 싣는다 — 총 **169개** (main 149 · test 19 · 기타 1).
```
src/adapter/inbound/graphql/build.gradle
src/config/architecture/modules.json (adapter-inbound-graphql 항목)
main:
src/main/java/dev/caskeleton/adapter/inbound/graphql/HealthGraphqlController.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/advanced/bootstrap/GraphQlAdvancedCapability.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/advanced/bootstrap/GraphQlAdvancedCapabilityDisabledException.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/advanced/bootstrap/GraphQlAdvancedFeatureFlags.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/advanced/bootstrap/GraphQlAdvancedModuleGuard.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/advanced/persisted/GraphQlPersistedOperation.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/advanced/persisted/GraphQlPersistedOperationId.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/advanced/persisted/GraphQlPersistedOperationStatus.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/advanced/persisted/GraphQlPersistedOperationTransition.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/advanced/rsocket/GraphQlRSocketAdmission.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/advanced/rsocket/GraphQlRSocketHandlerFactory.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/advanced/sse/GraphQlSseAdmission.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/advanced/sse/GraphQlSseHandlerFactory.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/advanced/websocket/GraphQlWebSocketAdmission.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/advanced/websocket/GraphQlWebSocketHandlerFactory.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/autoconfigure/GraphQlActivationEnvironmentPostProcessor.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/autoconfigure/GraphQlOffAutoConfigurationImportFilter.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/autoconfigure/GraphQlPlatformActuatorEndpoint.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/autoconfigure/GraphQlPlatformAutoConfiguration.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/autoconfigure/GraphQlPlatformConfigurationReport.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/autoconfigure/GraphQlPlatformSettings.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/autoconfigure/GraphQlPlatformStartupValidator.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/autoconfigure/GraphQlRetiredSafetyAxis.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/autoconfigure/GraphQlRootAutoConfiguration.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/compat/GraphQlChangeKind.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/compat/GraphQlClientOwnerApproval.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/compat/GraphQlCompatibilityPolicy.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/compat/GraphQlDeprecationGate.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/compat/GraphQlRemovalDecision.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/compat/GraphQlSchemaComparator.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/context/ActorRef.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/context/GraphQlDeadline.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/context/GraphQlIdentityFingerprinter.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/context/GraphQlRequestContext.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/context/TenantContext.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/cost/GraphQlComplexityCalculator.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/cost/GraphQlCostCatalog.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/cost/GraphQlDocumentComplexityScorer.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/cost/GraphQlDocumentShapeAnalyzer.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/cost/GraphQlFieldCostDescriptor.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/cost/GraphQlParserLimitPolicy.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/cost/GraphQlParserLimits.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/cost/GraphQlParserOptionsFactory.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/cost/GraphQlResolverWeight.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/cost/GraphQlResponseByteLimiter.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/cost/GraphQlResponseNodeCounter.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/cost/GraphQlRuntimeBudgetTracker.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/cost/GraphQlStructuralLimitPolicy.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/cost/GraphQlStructuralLimits.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/dataloader/GraphQlBatchChunker.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/dataloader/GraphQlBatchErrorPolicy.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/dataloader/GraphQlBatchExecutor.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/dataloader/GraphQlBatchPolicyRegistry.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/dataloader/GraphQlBatchResultMapper.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/dataloader/GraphQlBatchTimeoutException.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/dataloader/GraphQlDataLoaderFactory.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/dataloader/GraphQlMissingKeyPolicy.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/error/GraphQlErrorContext.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/error/GraphQlExceptionResolver.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/error/GraphQlInternalErrorMasker.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/error/GraphQlNullabilityContract.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/error/GraphQlRequestErrorMapper.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/error/GraphQlSubscriptionExceptionResolver.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/error/GraphQlWireError.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/execution/BoundedPreparsedDocumentProvider.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/execution/GraphQlCancellation.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/execution/GraphQlDeadlinePropagator.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/execution/GraphQlExecutionProfileValidator.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/execution/GraphQlOperationNameInterceptor.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/execution/GraphQlOperationNamePolicy.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/execution/GraphQlPreparsedCacheMetrics.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/execution/GraphQlPreparsedCachePolicy.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/execution/GraphQlResolverBudget.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/execution/GraphQlResolverCatalog.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/execution/GraphQlResolverDescriptor.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/execution/GraphQlTimeoutPolicy.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlAcceptHeader.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlExecutionOutcome.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlExtensionsPolicy.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlHttpExecutor.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlHttpOutcome.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlHttpProfile.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlHttpRequestEnvelope.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlHttpResponse.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlHttpResponseFactory.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlHttpResponsePolicy.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlHttpStatusMapper.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlJsonStructurePolicy.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlJsonValues.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlMediaTypes.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlRequestEnvelopeValidator.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlRequestSize.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/mutation/GraphQlCanonicalInput.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/mutation/GraphQlMutationContractValidator.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/mutation/GraphQlMutationFingerprint.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/mutation/GraphQlMutationIdempotencyInterceptor.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/observation/GraphQlDataLoaderObservationConvention.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/observation/GraphQlMetricCardinalityPolicy.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/observation/GraphQlOperationNameCardinality.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/observation/GraphQlProfilerAccessPolicy.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/observation/GraphQlRequestObservationConvention.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/observation/GraphQlResolverObservationConvention.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/observation/GraphQlSensitiveAttributeFilter.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/pagination/GraphQlConnectionAssembler.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/pagination/GraphQlCursorFraming.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/pagination/GraphQlCursorKeyRing.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/pagination/GraphQlCursorPayload.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/pagination/HmacGraphQlCursorCodec.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/policy/GraphQlClientPolicy.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/policy/GraphQlClientPolicyManifest.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/policy/GraphQlOperationCatalog.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/policy/GraphQlPolicyViolation.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/release/GraphQlReleaseEvidence.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/release/GraphQlReleaseGate.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/release/GraphQlReleaseOverride.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/release/GraphQlReleaseReportWriter.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/release/GraphQlStableCapabilityManifest.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/runtime/GraphQlBatchLoaderRegistrar.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/runtime/GraphQlBlockingBridge.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/runtime/GraphQlCostBudgetHandler.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/runtime/GraphQlDataFetcherExceptionResolver.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/runtime/GraphQlExecutionChain.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/runtime/GraphQlOperationSelectionHandler.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/runtime/GraphQlPlatformInstrumentation.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/runtime/GraphQlPlatformWebInterceptor.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/runtime/GraphQlPreparsedDocumentAdapter.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/runtime/GraphQlPrincipalResolver.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/runtime/GraphQlRequestObservationConventionAdapter.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/runtime/GraphQlWireErrorMapper.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/runtime/servlet/GraphQlRequestBodyLimitFilter.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/scalar/GraphQlDecimalBounds.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/scalar/GraphQlScalarWiringConfigurer.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/schema/GraphQlMappingInspectionGate.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/schema/GraphQlMappingIssue.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/schema/GraphQlMappingPolicy.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/schema/GraphQlOneOfInputValidator.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/schema/GraphQlOneOfPolicy.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/schema/GraphQlOneOfSchemaGate.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/schema/GraphQlScalarDefinition.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/schema/GraphQlScalarManifest.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/schema/GraphQlSchemaAssembler.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/schema/GraphQlSchemaAssemblyResult.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/schema/GraphQlSchemaContract.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/schema/GraphQlSchemaHash.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/security/ApplicationObjectAuthorization.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/security/GraphQlAuthorizationPolicy.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/security/GraphQlClientProfileResolver.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/security/GraphQlContextCleanup.java
src/main/java/dev/caskeleton/adapter/inbound/graphql/security/GraphQlContextPropagator.java
test:
src/test/java/dev/caskeleton/adapter/inbound/graphql/autoconfigure/GraphQlObservationWiringTest.java
src/test/java/dev/caskeleton/adapter/inbound/graphql/autoconfigure/GraphQlPolicyRequestPathTest.java
src/test/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlAcceptNegotiationTest.java
src/test/java/dev/caskeleton/adapter/inbound/graphql/http/GraphQlRequestBoundsTest.java
src/test/java/dev/caskeleton/adapter/inbound/graphql/runtime/GraphQlBatchLoaderRegistrationTest.java
src/testFixtures/java/dev/caskeleton/adapter/inbound/graphql/advanced/admin/InMemoryGraphQlPersistedOperationAdminPort.java
src/testFixtures/java/dev/caskeleton/adapter/inbound/graphql/advanced/persisted/InMemoryGraphQlPersistedOperationRegistry.java
src/testFixtures/java/dev/caskeleton/adapter/inbound/graphql/error/GraphQlPartialResponseFixture.java
src/testFixtures/java/dev/caskeleton/adapter/inbound/graphql/testkit/GraphQlContractViolation.java
src/testFixtures/java/dev/caskeleton/adapter/inbound/graphql/testkit/GraphQlDataLoaderContractSuite.java
src/testFixtures/java/dev/caskeleton/adapter/inbound/graphql/testkit/GraphQlDownstreamFailureFixture.java
src/testFixtures/java/dev/caskeleton/adapter/inbound/graphql/testkit/GraphQlHttpContractSuite.java
src/testFixtures/java/dev/caskeleton/adapter/inbound/graphql/testkit/GraphQlJpaIntegrationFixture.java
src/testFixtures/java/dev/caskeleton/adapter/inbound/graphql/testkit/GraphQlMongoIntegrationFixture.java
src/testFixtures/java/dev/caskeleton/adapter/inbound/graphql/testkit/GraphQlPaginationContractSuite.java
src/testFixtures/java/dev/caskeleton/adapter/inbound/graphql/testkit/GraphQlRequestContexts.java
src/testFixtures/java/dev/caskeleton/adapter/inbound/graphql/testkit/GraphQlSchemaContractSuite.java
src/testFixtures/java/dev/caskeleton/adapter/inbound/graphql/testkit/GraphQlSecurityContractSuite.java
src/testFixtures/java/dev/caskeleton/adapter/inbound/graphql/testkit/GraphQlStorageIntegrationEvidence.java
기타:
CLAUDE.md
해석되지 않은 인용 (12종) — 외부 타입·문서상 약칭 등:
evidence/raw/205-inbound-graphql-module-inventory.txt
evidence/raw/207-inbound-graphql-autoconfigure-probes.txt
evidence/raw/208-inbound-graphql-schema-probes.txt
evidence/raw/209-inbound-graphql-execution-probes.txt
evidence/raw/210-inbound-graphql-cost-security-probes.txt
evidence/raw/211-inbound-graphql-http-probes.txt
evidence/raw/212-inbound-graphql-data-probes.txt
evidence/raw/213-inbound-graphql-release-probes.txt
evidence/raw/214-inbound-graphql-advanced-streaming-probes.txt
evidence/raw/215-inbound-graphql-advanced-shaping-probes.txt
evidence/raw/216-inbound-graphql-advanced-platform-probes.txt
evidence/raw/217-inbound-graphql-testkit-probes.txt
```