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:
co-authored by
Claude Opus 5
parent
3b5aee50e3
commit
b074c1494e
@@ -85,8 +85,97 @@ composition root 는 이 leaf 를 채택할 때 인증/인가 및 CORS 정책을
|
||||
`spring.graphql.graphiql.enabled=false`,
|
||||
`spring.graphql.schema.introspection.enabled=false` 를 운영 설정으로 명시해야 한다.
|
||||
|
||||
## 아직 구현하지 않은 P2 범위
|
||||
---
|
||||
|
||||
이 leaf 와 현재 sample 에는 feature GraphQL schema/resolver, query depth/cost 제한, persisted
|
||||
operation, DataLoader/batching, subscription 이 구현되어 있지 않다. 이 항목들은 실제 GraphQL 제품
|
||||
표면을 채택할 때 별도 설계·테스트와 함께 추가해야 한다.
|
||||
# GraphQL API 실행 플랫폼 — 설계 결정의 근거
|
||||
|
||||
위 스켈레톤 머시너리 위에, GraphQL 설계 문서(Stable 48 Task / Advanced 19 Task)의 내용을
|
||||
이 leaf 의 sub-package 로 구현했다. 아래는 그 과정에서 내린 **되돌리기 어려운 결정**과 근거다.
|
||||
|
||||
## 설계서의 "모듈"을 Gradle 모듈로 만들지 않은 이유
|
||||
|
||||
설계서는 Stable 16 + Advanced 12 = 28개 Gradle 모듈을 전제한다. 그러나 이 레포의 leaf 정체성
|
||||
SSOT 는 `src/config/architecture/modules.json` 이고 **정확히 19개** 로 고정되어 있다. 28개를
|
||||
추가하면 레지스트리·`settings.gradle` fail-closed 검증·의존 게이트가 전부 깨지고, 이는 Prime
|
||||
Directive 5번(HARD-STOP)에 정면으로 저촉된다.
|
||||
|
||||
그래서 **모듈 = bounded sub-package** 로 매핑했다(선례: httpclient leaf 가 366개 java 파일을 같은
|
||||
방식으로 담는다). 대신 "패키지는 경계가 아니다"라는 통상의 약점을 기계 검증으로 메웠다 —
|
||||
`build/GraphQlStableModule`·`GraphQlAdvancedModule` 이 모듈 정체성과 허용 edge 를 값으로 선언하고,
|
||||
`GraphQlModuleBoundaryTest` 가 **실제 소스 트리를 스캔**해 Stable→Advanced import, core-api 의
|
||||
프레임워크 import, Stable edge 의 Advanced 참조를 실패시킨다. Gradle 이 해주던 일을 테스트가
|
||||
한다.
|
||||
|
||||
## ArchUnit/JPA 없이 아키텍처 규칙을 강제한 방법
|
||||
|
||||
`architecture/` 의 규칙(리졸버 경계, 전송 타입, `@Transactional` 금지, Entity/Document 반환 금지)은
|
||||
**리플렉션 + 단순명(simple name) 매칭**으로 구현했다. 인바운드 어댑터가 자신이 금지하는 대상
|
||||
(`jakarta.persistence`, `spring-tx`)에 의존해야 그걸 검사할 수 있다면 본말전도이기 때문이다.
|
||||
`GraphQlControllerTransactionRule` 이 애노테이션 타입이 아니라 단순명 `Transactional` 을 보는 것은
|
||||
이 때문이며, 의도된 트레이드오프다. `GraphQlResolverBoundaryRules` 는 스캔 대상 패키지가 비어
|
||||
있으면 **실패한다** — 검사할 게 없어서 통과하는 조용한 무력화를 막는다.
|
||||
|
||||
## 부분 실패는 200, 요청 실패는 4xx
|
||||
|
||||
`http/GraphQlHttpStatusMapper.V1` 은 검증 통과 후 발생한 필드 에러를 HTTP 200 + partial data 로
|
||||
매핑한다. GraphQL over HTTP 초안의 status 294 는 **채택하지 않았다**
|
||||
(`GraphQlHttpProfile.usesDraftPartialResponseStatus()` 가 `false` 로 못 박고, 초안 프로파일은
|
||||
`advanced/get/GraphQlHttpDraftCompatibilityReport` 가 "의도적 미채택"으로 기록한다). 초안 상태
|
||||
코드를 프로덕션 와이어 계약에 넣으면 초안이 바뀔 때 클라이언트가 깨진다.
|
||||
|
||||
이 때문에 `Map.copyOf` 를 응답 데이터 경로에 쓸 수 없다 — partial data 는 **정당하게 null 값을
|
||||
가진다**. `GraphQlExecutionOutcome`·`GraphQlHttpResponse`·`GraphQlContractResponse` 는 null 을
|
||||
허용하는 `LinkedHashMap` 복사를 쓴다.
|
||||
|
||||
## 요청 단위 DB 트랜잭션을 열지 않는다
|
||||
|
||||
GraphQL 한 요청은 여러 root field 를 담을 수 있고, 각 root 는 자기 use case 를 호출한다.
|
||||
요청 전체를 하나의 트랜잭션으로 묶으면 커넥션을 요청 수명만큼 점유하고 부분 실패 의미론이
|
||||
무너진다. `mutation/GraphQlMutationContractValidator.rejectRequestWideTransaction` 이 이를
|
||||
계약으로 강제하고, `GraphQlControllerTransactionRule` 이 컨트롤러의 `@Transactional` 을 막는다.
|
||||
|
||||
## 커서는 HMAC 서명된 버전 있는 keyset
|
||||
|
||||
`pagination/HmacGraphQlCursorCodec` 은 offset 이 아니라 keyset payload 를 담고, 버전과 서명을
|
||||
붙인다. 비교는 `MessageDigest.isEqual` 로 상수 시간이다. 클라이언트가 커서를 조작해 다른
|
||||
tenant/정렬 축으로 넘어가는 것을 막기 위함이며, 키 회전은 `GraphQlCursorKeyRing` 이 담당한다.
|
||||
|
||||
## DataLoader 는 요청 스코프, 캐시 키는 actor·tenant 지문으로 격리
|
||||
|
||||
`dataloader/GraphQlDataLoaderRequestRegistry` 는 요청마다 새 인스턴스를 만든다. 전역 캐시는
|
||||
tenant 간 데이터 누출 경로가 된다. `security/GraphQlBatchContext.cacheScope()` 는 raw tenant id 가
|
||||
아니라 **sha256 지문**을 캐시 스코프에 쓴다(로그·메트릭에 tenant 원문이 새지 않도록).
|
||||
|
||||
## 관측은 저-카디널리티 강제
|
||||
|
||||
`observation/GraphQlMetricCardinalityPolicy` 는 operation name·필드 좌표처럼 유한 집합만 태그로
|
||||
허용하고, 변수·인자·actor id 는 `GraphQlSensitiveAttributeFilter` 가 걸러낸다. GraphQL 은
|
||||
카디널리티 폭발이 쉬운 전송이라 이 게이트가 없으면 메트릭 백엔드가 먼저 죽는다.
|
||||
|
||||
## Advanced 는 전부 기본 비활성 + 등급제
|
||||
|
||||
`advanced/bootstrap/GraphQlAdvancedFeatureFlags` 는 기본 전부 off 다. EXPERIMENTAL 등급
|
||||
(RSocket, incremental delivery, HTTP GET draft)은 명시적 승인 플래그 없이는
|
||||
`GraphQlAdvancedModuleGuard` 가 production 활성화를 거부한다. 등급 승격은
|
||||
`advanced/release/GraphQlAdvancedPromotionDecision` 이 ADR 번호·승인자·미해결 증거를 요구한다 —
|
||||
"조용히 켜짐"을 구조적으로 불가능하게 만든다.
|
||||
|
||||
## 릴리스 게이트는 증거가 없으면 거부한다
|
||||
|
||||
`release/GraphQlReleaseGate` 와 `advanced/release/GraphQlAdvancedReleaseGate` 는 성능·장애·보안·
|
||||
호환성 증거가 없으면 **통과시키지 않는다**. Advanced 는 Stable 기준선 없이는 릴리스 자체가
|
||||
불가능하다. 같은 이유로 `graphqlPerformanceTest` 레인은 성능 태그가 하나도 없으면 실패한다 —
|
||||
증거 부재를 통과로 위장하지 않기 위한 fail-closed 설계다.
|
||||
|
||||
## 설계서의 내부 불일치 처리
|
||||
|
||||
Stable 16번째 모듈이 산문에서는 `graphql-testkit-security`, Task 1 파일 목록과 설계 §5 에서는
|
||||
`graphql-testkit-integration` 으로 서로 다르게 적혀 있다. **파일 목록 쪽(testkit-integration)을
|
||||
채택**하고, 보안 계약 표면은 `testkit/GraphQlSecurityContractSuite` 로 제공했다. 둘 다 실제로
|
||||
존재하므로 어느 쪽 독법이든 표면은 비지 않는다.
|
||||
|
||||
## 아직 구현하지 않은 범위
|
||||
|
||||
feature GraphQL schema/resolver 는 여전히 이 leaf 밖이다(스켈레톤은 health 표면만 소유).
|
||||
실부하 성능 증거와 실 datastore 통합 증거도 이 leaf 밖에서 생성해야 한다 — 다만 그 **부재가
|
||||
릴리스를 막도록** 게이트가 이미 서 있다.
|
||||
|
||||
Reference in New Issue
Block a user