feat(graphql): GraphQL API 실행 플랫폼 구현 (Stable 48 + Advanced 19 Task)

설계 문서(specs/2026-08-12-graphql-api-execution-platform-design.md)와 두 실행 계획서에
선언된 create path 전량을 adapter-inbound-graphql leaf 안에 구현한다.

- 계획서 main 클래스 322개 전량, Task별 테스트 클래스 67개(Stable 48 + Advanced 19) 전량.
- 설계서의 Stable 16 + Advanced 12 "Gradle 모듈"은 modules.json 이 19개 leaf 정체성을
  소유하므로 bounded sub-package 로 매핑한다(선례: httpclient leaf). 모듈 경계는 문서가
  아니라 GraphQlStableModule/GraphQlAdvancedModule 값 선언 + GraphQlModuleBoundaryTest 의
  실제 소스 스캔으로 기계 검증한다.
- architecture/ 규칙은 리플렉션 + 단순명 매칭으로 구현한다. 인바운드 어댑터가 자신이
  금지하는 jakarta.persistence/spring-tx 에 의존해야 검사할 수 있다면 본말전도이기 때문.
- 부분 실패는 HTTP 200 + partial data, 요청 실패는 4xx. GraphQL over HTTP 초안 status 294 는
  의도적으로 미채택(초안 변경이 클라이언트를 깨뜨리므로).
- 요청 단위 DB 트랜잭션을 열지 않는다. 커서는 HMAC 서명된 버전 있는 keyset(상수 시간 비교).
- DataLoader 는 요청 스코프, 캐시 키는 actor/tenant sha256 지문으로 격리.
- Advanced capability 는 전부 기본 비활성. EXPERIMENTAL 등급은 명시 승인 없이 production
  활성화가 거부된다.
- spring-webflux 는 compileOnly(runtimeClasspath 제외) — MVC 배치가 WebFlux 런타임을
  물려받지 않도록. lockfile 이 스코프 제한을 고정.
- graphqlPerformanceTest 는 성능 태그가 0개면 실패한다. failOnNoDiscoveredTests 는 태그
  필터로 0건이 된 경우를 잡지 못해(Gradle 9.0.0 실측) 결과 검사를 추가했다. 증거 부재를
  통과로 위장하지 않기 위한 fail-closed.

검증: graphqlStableTest 404 / graphqlContractTest 9 / graphqlAdvancedTest 141 tests,
:adapter:inbound:graphql:check, verifyCleanArchitectureDependencies,
CleanArchitectureTest, verifyConfigurationPropertiesProcessor, verifyEnvKeys,
verifyPublicPathSnapshot 전부 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-14 15:41:38 +09:00
co-authored by Claude Opus 5
parent 3b5aee50e3
commit b074c1494e
448 changed files with 28244 additions and 13 deletions
+57 -7
View File
@@ -13,11 +13,30 @@ Package root: `dev.caskeleton.adapter.inbound.graphql`.
코드 주석에서 덜어낸 **설계 결정의 근거**는 [README.md](README.md) 가 모아둔다 (이 문서는 모듈
규칙 SSOT).
## 플랫폼 모듈 = sub-package (Gradle 모듈 아님)
GraphQL 플랫폼 설계서는 Stable 16개 + Advanced 12개 "모듈"을 말하지만, 이 레포의 leaf 정체성
SSOT 는 `src/config/architecture/modules.json` 이고 거기에는 **정확히 19개 leaf** 만 존재한다.
따라서 설계서의 28개 모듈은 이 leaf 안의 **bounded sub-package** 로 매핑한다(선례: httpclient
leaf). 대신 모듈 경계는 문서가 아니라 기계가 지킨다:
- `build/GraphQlStableModule` · `build/GraphQlAdvancedModule` 이 모듈 정체성과 허용 의존 edge 를
값으로 선언하고, `build/GraphQlBuildModel` 이 실제 소스 트리를 스캔한다.
- `build/GraphQlModuleBoundaryTest` 가 (a) Stable 패키지의 `...graphql.advanced` import 금지,
(b) `graphql-core-api` 계열의 Spring/GraphQL Java/Reactor/persistence import 금지,
(c) Stable 의존 edge 가 Advanced 모듈을 가리키지 않을 것을 강제한다.
**새 플랫폼 sub-package 를 추가할 때는 반드시 해당 모듈 레코드에 정체성과 허용 edge 를 먼저
등록한다.** 등록 없이 추가된 패키지는 경계 테스트가 실패시킨다.
## Responsibility
- GraphQL 전송 인프라만: 최소 health 스키마(`skeleton.graphqls`) + `HealthGraphqlController`,
프로토콜 에러 매핑(`GraphqlExceptionResolver`). Spring for GraphQL 이 스키마와 컨트롤러를
자동 합성/바인딩하도록 얹는 얇은 계층이다.
- 그 위에 **GraphQL API 실행 플랫폼**(`...graphql` 하위 sub-package 군)이 스키마 계약·전송
프로파일·실행 정책·비용 한계·DataLoader·페이지네이션·에러/보안 경계·관측·릴리스 게이트를
소유한다. 플랫폼은 여전히 **feature-agnostic** 이며 정책·계약·검증 기계만 제공한다.
- feature-agnostic: `classpath:graphql/**` 스키마와 모든 `@Controller` `@QueryMapping`/
`@MutationMapping` 을 generic 하게 합성한다. **WorkLog 등 구체 기능을 이름으로 알지 않는다.**
- classpath opt-in: 현재 `app-bootstrap`/`sample-portfolio` production runtime 은 이 leaf 를
@@ -30,6 +49,13 @@ Package root: `dev.caskeleton.adapter.inbound.graphql`.
- `spring-boot-starter-graphql`, `spring-boot-starter-web`, `jackson-datatype-jsr310`
(전부 Spring Boot BOM 관리 — 버전 명시 없음).
- test scope 에 한해 실제 HTTP 인증/CORS qualification 용 `spring-boot-starter-security`.
- `compileOnly` 로만 `spring-webflux` — REACTIVE_WEBFLUX 전송 프로파일(`http/webflux/`)을
컴파일하기 위한 것이고, 의도적으로 `runtimeClasspath` 에서 제외한다. MVC 배치에 WebFlux 를
끌어들이지 않기 위함이며 `gradle.lockfile` 이 이 스코프 제한을 고정한다
(`spring-webflux:...=compileClasspath,testCompileClasspath,testRuntimeClasspath`).
- `annotationProcessor``spring-boot-configuration-processor``GraphQlPlatformProperties`
`@ConfigurationProperties` 이므로 레포 전역 `verifyConfigurationPropertiesProcessor` 패리티
게이트가 이 선언을 요구한다.
## Forbidden
@@ -59,15 +85,26 @@ feature 는 `ApiErrorCarrier` 를 구현한 예외(자신의 `ApiErrorCode` 를
현재 sample 에 feature GraphQL schema/controller/resolver 가 있다고 가정하지 않는다. 이 leaf 는
health 스키마만 소유한다.
## 명시적 미구현 범위(P2)
## 구현된 플랫폼 범위
- feature GraphQL schema/resolver
- query depth/cost 제한
- persisted operation
- DataLoader/batching
- subscription
query depth/cost 제한(`cost/`), persisted operation(`advanced/persisted/`),
DataLoader/batching(`dataloader/`), subscription(`advanced/subscription/`, `advanced/websocket/`,
`advanced/sse/`)은 **더 이상 미구현이 아니다.** 다만 이들은 정책·계약·검증 기계이며, 실제
composition root 가 채택할 때 정책 값과 인증/인가 빈을 함께 제공해야 한다.
이 범위는 production GraphQL 표면 채택 시 별도 설계와 qualification 을 요구한다.
여전히 미구현인 것:
- feature GraphQL schema/resolver — 이 leaf 는 health 표면만 소유한다(변경 없음).
- 실부하 성능/장애 시나리오 증거 — `release/GraphQlPerformanceScenario`,
`GraphQlFaultScenario` 는 시나리오 카탈로그를 정의하고 `GraphQlReleaseGate` 는 그 증거가
없으면 릴리스를 **거부**한다. 증거 자체는 실제 부하 인프라를 요구하므로 이 leaf 밖에서
생성한다(`graphqlPerformanceTest` 레인이 그 자리를 예약해 둔다).
- 실제 datastore 통합 증거 — `testkit/GraphQlJpaIntegrationFixture` /
`GraphQlMongoIntegrationFixture` 가 계약을 정의하고 `GraphQlStorageIntegrationEvidence`
증거를 요구한다. 실 datastore 기동은 persistence leaf 의 책임 범위다.
- Advanced capability 는 전부 **기본 비활성**이다(`advanced/bootstrap/GraphQlAdvancedFeatureFlags`).
EXPERIMENTAL 등급(RSocket, incremental delivery, HTTP GET draft)은 명시적 승인 없이는
`GraphQlAdvancedModuleGuard` 가 production 활성화를 거부한다.
## Test
@@ -78,3 +115,16 @@ cd src
--tests dev.caskeleton.adapter.inbound.graphql.GraphqlHttpBoundaryQualificationTest \
--console=plain
```
플랫폼 테스트 레인(`gradle/graphql-platform-conventions.gradle` 등록). 기본 `test`
`quarantine`·`graphql-performance` 태그를 제외한다:
```bash
./gradlew :adapter:inbound:graphql:graphqlStableTest --console=plain # 404 tests
./gradlew :adapter:inbound:graphql:graphqlContractTest --console=plain # 9 tests
./gradlew :adapter:inbound:graphql:graphqlAdvancedTest --console=plain # 141 tests
./gradlew :adapter:inbound:graphql:graphqlPerformanceTest --console=plain # 실부하 인프라 필요
```
`graphqlPerformanceTest``@Tag("graphql-performance")` 가 하나도 없으면 **실패한다** — 이는
버그가 아니라 "성능 증거 없음"을 통과로 위장하지 않기 위한 fail-closed 설계다.