feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가

This commit is contained in:
DongHyeonka
2026-08-15 13:01:58 +09:00
parent ac874e49e6
commit 2f5d2fc219
909 changed files with 62510 additions and 6354 deletions
@@ -0,0 +1,889 @@
# GraphQL 인바운드 모듈 상세 코드·아키텍처 리뷰
- 기준 일자: 2026-08-14
- 기준 Git HEAD: `ac874e49e608b35429f82aa098574b52a68f2069`
- 대상 Gradle leaf: `:adapter:inbound:graphql`
- 주 대상 경로: `src/adapter/inbound/graphql`
- 교차 확인 경로: `src/config/architecture/modules.json`, `src/gradle/graphql-platform-conventions.gradle`, `src/.gitignore`
- 판정: **CHANGES REQUIRED / 현재 컴파일 불가**
- 검토 방식: 전체 파일·import·production reference inventory, 핵심 실행 경로 정독, 세 개의 독립 병렬 리뷰, Gradle focused/architecture 검증
- 변경 범위: 이 리뷰 문서만 추가했다. production/test 코드는 수정하지 않았다.
리뷰 도중 HEAD가 `c3043e530a604315c4df341b87b5470c7617ea03`에서 위 commit으로 이동했지만,
GraphQL tree hash는 두 revision 모두 `bd8307364e2312814995e5f3bb1386cee37b1498`이고
`src/.gitignore`, GraphQL convention, architecture registry에도 delta가 없음을 확인했다.
## 1. 결론
현재 GraphQL leaf는 373개 production Java 파일과 75개 test Java 파일을 가진 큰 실행 플랫폼 후보지만,
두 층의 문제가 겹쳐 있다.
첫 번째는 즉시 고쳐야 하는 **빌드 차단**이다. `src/.gitignore`의 unanchored `build/` 규칙이 Gradle
산출물뿐 아니라 Java source package인 `...graphql.build`까지 무시한다. 문서와 production code는
`GraphQlBuildModel`, `GraphQlStableModule`, `GraphQlAdvancedModule`, `GraphQlModuleBoundaryTest`가 있다고
주장하지만 실제 checkout에는 없다. 그 결과 focused test는 test 실행 전에 `compileJava`에서 7개
오류로 실패하고, 단일 leaf 안의 Stable/Advanced 경계를 지킨다는 핵심 안전망도 함께 사라졌다.
두 번째는 더 근본적인 **런타임 진실성 문제**다. cost, authorization, DataLoader, cursor,
idempotency, observation, persisted operation, subscription 등 많은 정책과 값 객체가 구현되어 있지만,
대부분 Spring GraphQL이 실제 `/graphql` 요청을 처리하는 extension point에 연결되지 않는다. 현재 HTTP
qualification은 Spring Boot 기본 endpoint와 health controller/error resolver를 검증할 뿐, 이 플랫폼의
pipeline을 거치지 않는다. 따라서 unit test가 복구되어 green이 되더라도 “정책 객체가 맞다”는 증거와
“실제 요청에 정책이 강제된다”는 증거를 분리해야 한다.
즉시 적용할 원칙은 다음과 같다.
1. GQL-001을 단독 PR로 먼저 처리해 compile과 내부 경계 검사를 복구한다.
2. 복구 전후 모두 현재 artifact를 `runtime-ready GraphQL execution platform`으로 승격하지 않는다.
3. Spring 기본 `/graphql`을 canonical transport로 정하고, 정책을 공식 extension point에 연결한다.
4. 자체 MVC/WebFlux adapter를 실제 endpoint로 만들 계획이 없다면 제거한다. 평행 실행 경로를 두지 않는다.
5. repository 자동 노출은 Advanced라도 제거한다. application use case를 우회하는 예외를 만들지 않는다.
6. correctness/security red test를 먼저 고정한 뒤 public API와 Gradle leaf를 단계적으로 분리한다.
7. 실제 random-port request가 정책에 의해 거부되고 resolver/use case가 0회 호출됨을 promotion 증거로 삼는다.
## 2. 범위와 증거 경계
### 2.1 현재 규모
| 항목 | 현재 값 |
|---|---:|
| production Java 파일 | 373 |
| production Java LOC | 20,155 |
| test Java 파일 | 75 |
| test Java LOC | 8,424 |
| test annotation (`@Test`, `@ParameterizedTest`) | 524 |
| 최상위 production package | 21 |
| main resource | `graphql/skeleton.graphqls` 1개 |
| test resource | qualification schema 1개 |
| 외부 Spring/GraphQL/Reactor import를 가진 production Java | 19 |
최상위 package는 `advanced`, `api`, `architecture`, `autoconfigure`, `compat`, `context`, `cost`,
`dataloader`, `error`, `execution`, `fetch`, `http`, `mutation`, `observation`, `pagination`, `policy`,
`release`, `scalar`, `schema`, `security`, `testkit`이다.
373개 중 354개가 Spring/GraphQL Java/Reactor type을 직접 import하지 않는다는 점은 framework-free policy
model을 추출할 여지가 크다는 뜻이다. 동시에 거의 모든 최상위 type이 public이어서 현재 한 jar가
사실상 수백 개의 API를 노출한다.
### 2.2 검토 깊이
| Path | Status | Evidence | Extracted facts |
|---|---|---|---|
| `src/adapter/inbound/graphql/build.gradle` | READ_FULL | 1-54 | servlet runtime 의존, WebFlux compile-only, test lane 등록 |
| `src/gradle/graphql-platform-conventions.gradle` | READ_FULL | 1-100 | Stable/contract/Advanced/performance lane과 누락된 boundary model 주장 |
| `src/adapter/inbound/graphql/CLAUDE.md` | READ_FULL | 1-143 | 단일 leaf 내부 28 bounded package, runtime opt-in, 실행 범위·검증 주장 |
| `src/adapter/inbound/graphql/README.md` | READ_FULL | 1-189 | health endpoint, error mapping, 설정·경계·Advanced 설계 근거 |
| `src/config/architecture/modules.json` GraphQL record | READ_FULL | GraphQL leaf record | 허용 project edge와 빈 runtime membership |
| `src/.gitignore` | READ_FULL | 1-16 + `git check-ignore` | `build/`가 Java source package까지 무시하는 직접 원인 |
| root controller/error resolver/schema | READ_FULL | production + 대응 tests | 현재 실제 Spring GraphQL endpoint 표면 |
| `autoconfigure`, `http`, `execution`, `architecture` | READ_FULL | production 핵심 경로 + 대응 tests | auto-config 등록, 실행 연결, transport, 경계 검사 |
| `cost`, `security`, `dataloader`, `pagination`, `mutation` | READ_FULL | 핵심 policy/codec/executor + 대응 tests | 구조 제한, tenant/auth, batch, cursor, idempotency correctness |
| `schema`, `compat`, `scalar`, `error`, `observation` | READ_FULL | production 핵심 경로 + 대응 tests | schema 조립/호환, scalar, wire error, cardinality |
| `advanced/**` | READ_PARTIAL | public entry/state transition/production reference scan + 주요 tests | persisted/admin/codegen/subscription/federation/transport seam |
| `release/**`, `testkit/**` | READ_PARTIAL | public contract/lane/reference scan + suite tests | self-reported evidence와 production artifact 오염 |
| production 373개/test 75개 전체 | READ_PARTIAL | inventory/import/reference/public-surface scan | 파일·package·사용처·실행 연결의 전수 정적 탐색 |
이 문서는 28,579 LOC의 모든 method를 line-by-line 승인한 결과가 아니다. 전체 inventory와 reference scan을
바탕으로 실행 seam과 고위험 policy를 정독한 구조·correctness 리뷰다. `advanced/**`, release/testkit의
세부 알고리즘은 명시한 범위 밖에서 `UNVERIFIED`이며, 실제 adopter/runtime·load·fault evidence도 없다.
## 3. 유지할 설계
리팩터링 과정에서 다음은 보존할 가치가 있다.
- registry상 GraphQL leaf의 production project dependency가 Clean Architecture 방향을 벗어나지 않는다.
- `runtime_memberships`가 비어 있어 현재 app-bootstrap/sample runtime에 조용히 유입되지 않는다.
- 실제 `HealthGraphqlController`는 얇고 feature/domain/repository 지식이 없다.
- 실제 Spring exception resolver는 shared error code만 노출하고 raw exception message를 사용하지 않는다.
- schema compatibility를 SDL 문자열 diff가 아니라 AST로 비교하고 결과를 결정적으로 정렬한다.
- partial data map에 null을 허용하는 defensive copy를 사용한다. 이를 `Map.copyOf`로 바꾸면 안 된다.
- cursor HMAC을 `MessageDigest.isEqual`로 비교하고 query/filter에 bind하려는 방향은 맞다.
- DataLoader 결과에서 `Present`, `Missing`, `Failed`를 구분하려는 결과 algebra는 유용하다.
- document traversal은 fragment cycle과 방문 node budget을 고려한다.
- Advanced capability가 기본 비활성이고 experimental production activation을 명시적으로 거부한다.
- test lane이 빈 performance evidence를 success로 위장하지 않으려는 fail-closed 의도는 좋다.
- broad static import scan에서 Stable package가 `...graphql.advanced`를 직접 import하는 edge와
production repository/JPA/Spring Data 직접 사용은 발견되지 않았다.
## 4. 우선순위 요약
| ID | 우선순위 | 주제 | 완료 조건 |
|---|---|---|---|
| GQL-001 | P0 | ignored `build` source package 때문에 compile 및 경계 모델 소실 | 비-ignore package로 모델 복구, compile/test/boundary negative fixture 통과 |
| GQL-002 | P0 | 플랫폼 정책이 실제 `/graphql` 실행 경로에 미연결 | real interceptor/instrumentation/DataLoader/wiring E2E에서 정책 거부 증명 |
| GQL-003 | P1 | auto-configuration 등록·binding default·실제 bean 검증 불일치 | 무설정 boot, imports metadata, 실제 override bean validation 통과 |
| GQL-004 | P1 | servlet artifact가 reactive profile도 표방 | MVC/WebFlux runtime classpath와 context가 별도 leaf에서 독립 통과 |
| GQL-005 | P1 | request byte 제한 미강제와 valid null variable 거부 | decode 전 body cap, null/omitted/value E2E 통과 |
| GQL-006 | P1 | `Accept` q-value/q=0 무시 | quality/specificity 기반 negotiation contract 통과 |
| GQL-007 | P1 | named fragment introspection 우회와 variable nesting 공백 | reachable fragment/variable JSON budget 거부 E2E 통과 |
| GQL-008 | P1 | resolver 경계 검사가 generic/JAR/subpackage를 놓치고 reactive type을 오판 | actual controller graph와 recursive generic negative fixture 통과 |
| GQL-009 | P1 | Advanced repository 자동 노출이 canonical hard-stop과 충돌 | repository exposure API 제거, application handler만 허용 |
| GQL-010 | P1 | cursor framing·rotation·direction·tenant binding 결함 | versioned codec property tests와 active-key/scope rejection 통과 |
| GQL-011 | P1 | mutation fingerprint collision과 tenant 없는 idempotency scope | typed canonical serialization과 tenant/version scope 테스트 통과 |
| GQL-012 | P1 | error resolver와 category contract가 두 벌 | 하나의 mapper를 모든 Spring/transport path가 사용 |
| GQL-013 | P1 | persisted-operation admin 상태·감사·인가가 durable하지 않음 | authenticated principal, CAS state machine, atomic audit contract 통과 |
| GQL-014 | P1 | codegen이 operation을 검증하지 않고 generator도 code를 생성하지 않음 | executable document validation 또는 정직한 planner 명명 |
| GQL-015 | P1 | schema comparator가 kind/default/extension/applied directive를 놓침 | breaking matrix와 extension ownership tests 통과 |
| GQL-016 | P1 | custom DataLoader와 timeout이 실제 loader 실행을 강제하지 않음 | Spring registry 연결과 real query-count/deadline test 통과 |
| GQL-017 | P1 | MVC concurrency/context와 WebFlux blocking bridge가 안전하지 않음 | bounded admission, context propagation, event-loop nonblocking 증명 |
| GQL-018 | P1 | pipeline stage 순서가 필요한 정보와 모순 | authenticate→parse/select→authorize→cost→execute executable chain |
| GQL-019 | P1 | subscription/replay/drain lifecycle의 race와 scope 공백 | atomic state/lease, actor+tenant+subscription binding 경쟁 test 통과 |
| GQL-020 | P2 | preparsed cache expiry 미사용·global miss serialization | expiry/single-flight/parallel-key test 통과 |
| GQL-021 | P2 | cancellation hook 하나가 나머지 cleanup을 막음 | all-hooks-once + suppressed exception contract 통과 |
| GQL-022 | P2 | scalar input/output bounds와 expansion limit 불일치 | BigDecimal/Long 양방향 boundary test 통과 |
| GQL-023 | P2 | raw operation name metric cardinality와 실제 Observation 미연결 | registered-name/`other` bound와 real MeterRegistry test 통과 |
| GQL-024 | P2 | testkit/fixed secret/in-memory 구현이 main jar에 포함 | test fixtures/optional leaf 분리 및 jar surface gate 통과 |
| GQL-025 | P2 | 373개 type의 과도한 public surface와 한 leaf의 낮은 응집도 | api/spi allowlist와 6~8 capability leaf 독립 compile/test |
| GQL-026 | P2 | GraphQL context/storage SPI ownership이 dependency 방향과 충돌 | inbound-local mapping과 transport-neutral operational port로 분리 |
| GQL-027 | P3 | 문서·설정 namespace·test count·runtime 지원 주장 drift | generated metadata/runtime adoption test 기반 문서 동기화 |
## 5. 상세 발견 사항과 구현 명세
### GQL-001 — `build` Java package가 `.gitignore`에 걸려 compile과 경계 검사가 함께 사라졌다
**근거**
- `src/.gitignore:2`는 root에 고정되지 않은 `build/` 패턴이다.
- `git check-ignore -v --no-index
src/adapter/inbound/graphql/src/main/java/dev/caskeleton/adapter/inbound/graphql/build/GraphQlBuildModel.java`
는 `src/.gitignore:2:build/`를 반환한다.
- `GraphQlPlatformAutoConfiguration.java:3,107-108`과
`advanced/bootstrap/GraphQlAdvancedDependencyRules.java:3,26-28,43`은 존재하지 않는
`dev...graphql.build.GraphQlBuildModel`을 참조한다.
- `CLAUDE.md:36-40`, `README.md:108-115`, `graphql-platform-conventions.gradle:11-13`은
`GraphQlStableModule`, `GraphQlAdvancedModule`, `GraphQlBuildModel`,
`GraphQlModuleBoundaryTest`가 실제 tree를 검사한다고 기록하지만 네 파일은 main/test tree에 없다.
- focused `:test`는 `compileJava`에서 해당 package/class 관련 7개 오류로 실패했다.
**실패 모드**
로컬 작성자가 ignored package 아래 파일을 생성하면 파일이 보이므로 잠시 compile될 수 있지만 commit에
들어가지 않는다. fresh checkout/CI에서는 소스가 사라져 compile이 깨진다. 더 위험한 변형은 production
참조를 지웠을 때다. build는 green이 될 수 있지만 Stable→Advanced/core purity/등록 package 검사가 없는
false green이 된다.
**구현 결정**
1. source package 이름을 `...graphql.build`가 아니라 `...graphql.moduleboundary`로 바꾼다. `.gitignore`
예외보다 역할이 분명하고 다른 도구의 `build` 디렉터리 규칙과 충돌하지 않는다.
2. 세 production model을 복원한다. 단, source-tree scanner가 runtime에 필요하지 않으면
`GraphQlBuildModel`을 test/build logic으로 이동하고 `GraphQlPlatformAutoConfiguration`의 runtime
source scan을 제거한다.
3. `GraphQlModuleBoundaryTest`는 실제 source/import graph를 검사하며 다음 세 negative fixture를 가진다.
Stable→Advanced import, core package의 Spring/GraphQL/Reactor import, 등록되지 않은 package.
4. `graphqlStableTest`가 해당 FQCN을 명시적으로 포함하고, test가 0개면 실패하게 유지한다.
5. repository-level gate에 `src/**/src/{main,test}/java/**/build/**` 같은 ignored source-package를
탐지하는 검사를 추가한다. Git에 존재하지 않는 파일을 CI가 찾을 수 없으므로, package naming rule과
required boundary-class existence 검사를 함께 둔다.
**필수 테스트/검증**
```bash
cd src
./gradlew :adapter:inbound:graphql:compileJava --console=plain
./gradlew :adapter:inbound:graphql:test --console=plain
./gradlew :adapter:inbound:graphql:graphqlStableTest --console=plain
./gradlew :adapter:inbound:graphql:test \
--tests '*GraphQlModuleBoundaryTest' --rerun-tasks --console=plain
```
### GQL-002 — 정책 카탈로그는 크지만 실제 `/graphql` 요청에는 실행되지 않는다
**근거**
- `GraphQlPlatformAutoConfiguration.java:43-103`은 startup validator, stage 목록, mapping gate,
observation convention POJO를 bean으로 만들지만 실제 request hook을 등록하지 않는다.
- `GraphQlExecutionPipeline`은 실행 가능한 Chain of Responsibility가 아니라 enum stage 순서 record다.
- MVC/WebFlux transport adapter의 `handle()`은 controller/router/filter가 아니며 production 호출처가 없다.
- production에는 `WebGraphQlInterceptor`, GraphQL Java `Instrumentation`, Spring
`BatchLoaderRegistry`, 실제 `PreparsedDocumentProvider` 연결이 없다.
- `GraphQlScalarWiringConfigurer`는 올바른 `RuntimeWiringConfigurer` 구현이지만 production bean이 아니다.
- HTTP qualification은 Spring Boot 기본 `/graphql`, health controller, root exception resolver와
test-only security를 검증한다. platform auto-configuration과 custom adapters를 import하지 않는다.
**실패 모드**
adopter가 최대 depth/complexity, introspection, authorization, timeout, DataLoader policy를 설정하고
안전하다고 판단해도, Spring 기본 endpoint는 이 객체들을 호출하지 않는다. unit test는 각 policy 함수가
정상임만 증명하고 endpoint adoption을 증명하지 못한다.
**구현 결정: Spring-native 단일 실행 경로**
1. Spring 기본 `/graphql`을 canonical HTTP transport로 유지한다.
2. `GraphQlPlatformWebInterceptor implements WebGraphQlInterceptor`에서 인증 principal을 검증된
request context로 매핑하고 GraphQL/Reactor context에 넣는다.
3. `GraphQlPlatformInstrumentation` 또는 `ExecutionGraphQlService` decorator에서 document
parse/selection, introspection, authorization, cost, deadline/cancellation을 실행한다.
4. `RuntimeWiringConfigurer`, `BatchLoaderRegistry` 등록/decorator, actual preparsed document provider,
canonical exception resolver를 auto-configuration이 bean으로 조립한다.
5. 현재 MVC/WebFlux custom adapters는 제거한다. 자체 transport가 반드시 필요하다면 Spring 기본
handler를 끄고 실제 route를 소유하게 하며, 두 경로를 동시에 두지 않는다.
6. 모든 policy stage는 `GraphQlExecutionRequest`와 `GraphQlExecutionContext`를 입력·출력하는 실행 가능한
handler로 바꾼다. 단순 stage catalog는 문서/검증 view로만 파생한다.
**필수 E2E**
- random-port servlet `/graphql`에서 depth/cost/alias/introspection/oversize/authz/timeout 거부.
- 각 거부에서 controller, use case, batch loader 호출 횟수 0.
- actor/tenant/deadline이 controller와 DataLoader에 동일하게 전달됨.
- custom scalar를 포함한 schema boot 및 실제 coercion.
- 같은 document cache hit, request별 DataLoader cache 격리.
- reactive artifact를 유지한다면 동일 contract를 reactive random-port에서도 실행.
Spring GraphQL이 제공하는 공식 연결점은
[`WebGraphQlInterceptor`](https://docs.spring.io/spring-graphql/reference/1.3/request-execution.html),
[`RuntimeWiringConfigurer`](https://docs.spring.io/spring-graphql/docs/current/api/org/springframework/graphql/execution/RuntimeWiringConfigurer.html),
[`BatchLoaderRegistry`](https://docs.spring.io/spring-graphql/docs/current/api/org/springframework/graphql/execution/BatchLoaderRegistry.html)다.
구현 시 repository lock의 Spring GraphQL 2.0.0/Boot 4.0.0 API signature로 다시 확인한다.
### GQL-003 — auto-configuration, binding default, 실제 override 검증이 각각 다른 계약이다
**근거**
- `GraphQlPlatformAutoConfiguration`은 이름과 달리 `@Configuration`이며 auto-configuration imports
metadata가 없다. main resource는 schema 한 개뿐이다.
- `GraphQlPlatformProperties` primitive binding default는 `maximumPageSize=0`,
`maximumComplexity=0`인데 startup validator는 양수만 허용한다.
- `productionDefaults()` factory는 Spring binder default가 아니다.
- custom `backend.graphql.graphiql-enabled/introspection-enabled`와 실제
`spring.graphql.*` framework flags가 분리되어 있다.
- startup check는 주입된 override pipeline이 아니라 `GraphQlExecutionPipeline.stable()` 상수를 검증한다.
**구현 결정**
1. 재사용 starter라면 `@AutoConfiguration(after = GraphQlAutoConfiguration.class)`과
`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`를 추가한다.
내부 composition 전용이면 이름을 `GraphQlPlatformConfiguration`으로 바꾸고 app-bootstrap에서 명시 import한다.
2. properties를 nested record/class로 나누고 binder가 실제로 사용하는 default를 선언한다.
3. framework `GraphQlProperties`를 SSOT로 삼거나 custom flag와의 불일치를 startup failure로 만든다.
4. startup validator는 실제 주입된 pipeline, scalar manifest, client policies, key ring을 검증한다.
5. `ApplicationContextRunner`로 enabled/disabled/servlet/reactive/unsafe override matrix를 고정한다.
**필수 테스트**
- 아무 `backend.graphql.*`도 없는 context가 safe default로 부팅한다.
- custom/framework GraphiQL·introspection 값이 모순되면 부팅 실패한다.
- unsafe custom pipeline override가 startup에서 거부된다.
- auto-configuration imports와 configuration metadata에 모든 property가 존재한다.
### GQL-004 — 하나의 artifact가 servlet runtime을 강제하면서 reactive profile도 표방한다
**근거**
- `build.gradle:19-20`은 `spring-boot-starter-web`을 production implementation으로 둔다.
- WebFlux는 `compileOnly`라 reactive runtime에는 없다.
- `GraphQlWebFluxAutoConfiguration`은 application이 이미 reactive일 때만 활성화된다.
- reactive config는 blocking executor를 `Mono.fromCallable`로 감싸며 scheduler를 바꾸지 않는다.
- request validator 기본 bean은 MVC config 안에 있어 reactive context에서 기본 생성되지 않는다.
**구현 결정**
공통 artifact에 두 runtime을 넣지 말고 다음처럼 나눈다.
- `graphql-spring-execution`: Spring GraphQL execution/interceptor/wiring. servlet/reactive server 없음.
- `graphql-transport-mvc`: 위 leaf + `starter-web`.
- `graphql-transport-webflux`: 위 leaf + `starter-webflux`.
reactive profile은 `GraphQlReactiveExecutor` 전용 interface를 필수로 한다. blocking bridge가 필요하면 명시적
opt-in, bounded scheduler, bulkhead, lifecycle bean과 thread assertion을 함께 둔다.
### GQL-005 — request limit은 역직렬화 전에 강제되지 않고 valid null variable은 NPE가 된다
**근거**
- size policy 메서드는 존재하지만 custom adapters는 `validateEnvelope()`만 호출한다.
- 이미 materialized된 `GraphQlHttpRequestEnvelope`를 받으므로 JSON allocation 전 body cap을 적용할 수 없다.
- `GraphQlHttpRequestEnvelope:23-25`는 variables/extensions에 `Map.copyOf`를 사용해 null value를 거부한다.
- nested map/list는 shallow copy여서 생성 뒤 mutation 가능한 TOCTOU도 남는다.
**구현 결정**
1. servlet filter/reactive web filter 또는 bounded decoder에서 raw HTTP body byte cap을 먼저 적용한다.
2. JSON decode 후 query/variables/extensions를 UTF-8 byte 기준으로 검증한다.
3. variables/extensions는 null-preserving deep immutable JSON value copy를 사용한다.
4. JSON nesting, object key 수, list length에도 별도 bound를 둔다.
**필수 테스트**
- ASCII와 다중바이트 UTF-8의 exact limit/limit+1.
- variables의 omitted/explicit null/non-null 세 의미가 actual coercion까지 보존됨.
- nested original map/list 변경이 envelope에 반영되지 않음.
- oversize body는 decoder/controller/use case 0회와 413.
### GQL-006 — `Accept` 협상에서 client priority와 명시적 거부를 무시한다
**근거**
`GraphQlMediaTypes:45-63`은 parameter를 제거하고 server preference를 먼저 순회한다. 따라서
`application/graphql-response+json;q=0, application/json;q=1`에도 q=0인 첫 media type을 반환한다.
**구현 결정**
Spring `MediaType.parseMediaTypes`로 parse하고 quality/specificity를 정렬한 뒤 q=0을 제외한다. GraphQL
over HTTP profile이 생산 가능한 두 type과 client order의 교집합을 선택하고, malformed/empty/wildcard
정책을 명시한다. GraphQL over HTTP draft도 client가 제시한 우선순위를 존중하도록 요구한다
([GraphQL over HTTP draft](https://graphql.github.io/graphql-over-http/draft/)).
### GQL-007 — named fragment가 custom introspection gate를 우회하고 variable 입력 구조는 측정하지 않는다
**근거**
- `GraphQlDocumentShapeAnalyzer:96-132`의 introspection walk는 Field/InlineFragment만 처리한다.
- 같은 class의 일반 shape walk는 `FragmentSpread`와 cycle path를 처리한다.
- input nesting은 document literal만 보며 variables JSON은 보지 않는다.
- analyzer는 selected operation이 아니라 document의 모든 operation을 합산한다.
**실패 입력**
```graphql
query Q { ...I }
fragment I on Query { __schema { types { name } } }
```
**구현 결정**
operationName으로 선택한 operation과 reachable fragment graph만 하나의 budgeted walker가 순회하도록
합친다. introspection은 custom walker 하나만 신뢰하지 말고 engine validation/field visibility에서도
차단한다. variables는 streaming JSON constraint로 별도 제한한다.
### GQL-008 — resolver 경계 검사는 실제 adopter graph를 보지 못하고 valid reactive query도 거부한다
**근거**
- controller inspector와 boundary rules는 raw `Class<?>`만 검사해 `List<Entity>`, `Mono<Entity>`,
`Optional<Repository>`의 generic 내부 타입을 놓친다.
- package scan은 `file:` protocol, 직접 자식 `.class`만 지원해 JAR/subpackage를 건너뛴다.
- tests는 고정 fixture package만 직접 호출하고 production startup caller가 없다.
- `Publisher`를 subscription 외에서 모두 거부하지만 Spring GraphQL controller는 Query/Mutation에서
`Mono<T>`와 async return을 지원한다
([Spring GraphQL annotated controllers](https://docs.spring.io/spring-graphql/reference/controllers.html)).
**구현 결정**
1. build-time에는 ArchUnit/bytecode scan으로 실제 configured controller packages를 재귀 검사한다.
2. runtime에는 ApplicationContext의 실제 GraphQL controller bean/method를 startup 검사한다.
3. Java `Type`을 재귀 순회해 parameterized/array/wildcard/type-variable bound를 본다.
4. Query/Mutation에는 single-value async(`Mono`, `CompletionStage`)를 허용하고 multi-value publisher만
Subscription에 제한한다.
5. suffix-only `Repository/Dao` 휴리스틱은 보조 신호로 낮추고 package/assignability/annotation 증거를 쓴다.
### GQL-009 — repository 자동 노출은 Advanced여도 이 저장소의 Clean Architecture를 위반한다
**근거**
`advanced/compat/GraphQlRepositoryExposureValidator`와 `GraphQlRepositoryAllowlist`는 allowlisted
repository가 GraphQL field를 직접 back하는 경로를 지원하고 test도 이를 정상으로 고정한다. 현재 실제
controller가 repository를 직접 호출하는 위반은 없지만, 지원 계약 자체가 root HARD-STOP과 충돌한다.
**구현 결정**
- `SPRING_DATA_COMPAT`, repository exposure API와 정상 test를 제거한다.
- 자동 resolver 대상은 application query/use-case handler로 한정한다.
- 생성 resolver의 constructor/field/method generic graph에 repository, Spring Data interface,
persistence entity가 있으면 allowlist와 무관하게 실패한다.
- business transaction과 authorization은 application use case에 남긴다.
### GQL-010 — cursor는 서명되지만 정상 payload가 round-trip되지 않고 rotation/scope 검증도 불완전하다
**근거**
- keyset은 `;`, `=`, `|`, `\`를 escape하지만 decoder는 escape-aware하지 않은 `split`을 먼저 한다.
- queryProfile/filterFingerprint/keyId는 escape조차 하지 않는다.
- `GraphQlCursorKeyRing.activeKeyId()`는 production/test에서 사용처가 없고 기본 factory는 항상
`cursor-key-1`을 payload에 넣는다.
- connection request decode는 payload direction과 request direction을 비교하지 않는다.
- cursor는 tenant/actor scope를 bind하지 않는다.
- `keyIds()`는 mutable backing key set을 반환한다.
**구현 결정: versioned Codec Strategy**
1. v2 payload를 canonical JSON/CBOR 또는 length-prefixed typed framing으로 만든다.
2. codec이 active key id를 선택하고 envelope에 기록한다. caller payload가 signing key를 선택하지 않는다.
3. decode 입력에 expected query/filter/direction/tenant-scope fingerprint를 포함한다.
4. v1 decode를 migration 기간에만 유지하고 v2만 발급한다.
5. key ring map/key set을 완전 불변으로 만들고 secret clone은 유지한다.
6. `forTests()`와 fixed secret은 test fixtures로 이동한다.
**필수 property tests**
- 모든 string field의 delimiter/backslash/unicode round-trip.
- active key2로 신규 발급, key1 과거 cursor 검증, unknown/retired key 거부.
- forward↔backward, tenant A↔B, filter/query 변경 거부.
- tamper, truncation, oversized token, malformed Base64 거부.
### GQL-011 — mutation fingerprint canonical form이 충돌하고 tenant를 scope에 포함하지 않는다
**근거**
`GraphQlMutationFingerprint:29-33`은 top-level key만 정렬해 `key=value;`를 연결한다. 예를 들어
`{a:"b;c=d"}``{a:"b", c:"d"}`가 같은 canonical text가 된다. nested map은 재귀 정렬되지 않는다.
idempotency scope는 actor/coordinate/key만 포함하고 tenant와 contract version은 없다.
**구현 결정**
- recursive key sorting, JSON type, length framing, null/number normalization을 가진 canonical serializer를
하나의 port/service로 둔다.
- tenant fingerprint와 contract version을 scope에 포함한다.
- actor/tenant의 단순 SHA-256 prefix를 비가역이라고 부르지 않는다. 저엔트로피 identifier에는
rotation 가능한 HMAC fingerprint를 사용하고 metric label에는 넣지 않는다.
- `requireSingleUseCase`는 정확히 1을 요구하거나 실제 architecture gate로 교체한다.
### GQL-012 — error contract가 두 resolver와 여러 category vocabulary로 분기한다
**근거**
- root `GraphqlExceptionResolver`만 실제 Spring `DataFetcherExceptionResolverAdapter``@Component`다.
- `error/GraphQlExceptionResolver`는 richer masking/mapping을 제공하지만 Spring path에 연결되지 않는다.
- auth/cursor/idempotency/batch/timeout 예외의 code/category/retryable/executionId 계약이 경로마다 다르다.
- 대소문자만 다른 두 class 이름은 import 실수를 유발한다.
**구현 결정: Mapper + Adapter**
`GraphQlWireErrorMapper`를 canonical pure mapper로 두고 `GraphQlDataFetcherExceptionResolver`가 Spring
`GraphQLError`로 adapt한다. request-level HTTP failure와 field failure는 별도 strategy를 쓰되 code,
category, retryability, masking catalog는 공유한다. unknown failure의 raw message는 어떤 path에서도
노출하지 않는다.
### GQL-013 — persisted operation admin은 존재하지 않는 변경을 성공으로 audit할 수 있다
**근거**
- in-memory registry의 absent `updateStatus`는 no-op인데 admin service는 `ABSENT→BLOCKED/DEPRECATED` audit을 남긴다.
- `remove()`는 실제 삭제가 아니라 BLOCKED 전환이다.
- BLOCKED에서 DEPRECATED로 바꿔 다시 executable하게 만들 수 있는 transition guard가 없다.
- raw operator 문자열 allowlist를 받고 credential kind 거부 메서드는 service가 호출하지 않는다.
- registry 변경과 in-memory `ArrayList` audit은 원자적이지 않고 thread-safe하지 않다.
**구현 결정: State + authenticated command + durable transaction**
1. transport가 만든 `GraphQlAdminPrincipal`만 service에 전달한다.
2. lifecycle transition table을 두고 BLOCKED는 explicit audited unblock 전까지 terminal로 취급한다.
3. registry command는 updated record/version을 반환하거나 not-found/conflict를 던진다.
4. mutation과 audit append를 하나의 durable transactional port로 묶는다.
5. soft delete가 의도면 `remove``retireAndBlock`으로 이름 바꾼다.
### GQL-014 — codegen validator는 operation document를 읽지 않고 generator는 source를 만들지 않는다
**근거**
`GraphQlClientOperationGenerator.validateOperation`은 nonblank만 확인한 뒤 schema를 자기 자신과 비교한다.
`operationDocument`는 검증에 쓰지 않는다. invalid syntax나 unknown field operation이 통과한다. 다른
generator/factory도 실제 handler/source가 아니라 metadata set/report만 반환하는 사례가 많다.
**구현 결정**
- schema를 executable schema로 만들고 GraphQL Java parser/validator로 selected operation을 검증한다.
- 실제 source writer/Gradle task가 없다면 class/package를 `codegen-plan` 또는 `compatibility-policy`
정직하게 이름 바꾼다.
- invalid syntax, unknown field/argument/type, operation name ambiguity, valid fragment operation을 테스트한다.
### GQL-015 — schema compatibility와 ownership이 breaking change를 놓친다
**근거**
- 동일 이름의 `type Foo``input Foo` 같은 kind change를 먼저 비교하지 않는다.
- 기존 argument/input field의 default 추가·제거·변경을 비교하지 않는다.
- `extend type/interface/input/enum/union`의 field/member ownership과 duplicate를 충분히 기록하지 않는다.
- applied directive 변경이 아니라 directive definition만 비교한다.
- scalar SDL print 차이를 coercion change라 부르지만 실제 `Coercing` 구현 교체는 보지 못하고 description
변화는 오탐할 수 있다.
**구현 결정**
1. registry를 extension까지 normalize하거나 executable schema로 compile한 canonical model을 비교한다.
2. `TYPE_KIND_CHANGED`, `INPUT_DEFAULT_REMOVED/CHANGED/ADDED`, applied-directive change를 명시한다.
3. nested list/non-null 변화는 input/output position별 방향성을 재귀 분류한다.
4. scalar coercion compatibility는 SDL이 아니라 scalar manifest codec/version 계약으로 분리한다.
### GQL-016 — custom DataLoader contract는 실제 N+1과 timeout을 보장하지 않는다
**근거**
- `GraphQlDataLoaderRequestRegistry``Object` map이며 Spring/Java DataLoader registry에 연결되지 않는다.
- contract suite는 caller가 전달한 observed query count를 검사하고 test는 임의 숫자 1/2를 넘긴다.
- batch executor는 synchronous chunk 호출 전에만 시간을 보고 long/final chunk를 중단하지 못한다.
- mapped loader의 null은 `Present(null)`, ordered loader의 null은 `Missing`으로 해석되어 의미가 다르다.
- result cardinality가 같아도 requested key 대신 다른 key가 들어간 map을 검출하지 못한다.
**구현 결정: Spring registry adapter + Decorator**
Spring `BatchLoaderRegistry`에 실제 loader를 등록하고 chunk/timeout/auth scope/observation을 loader decorator로
적용한다. loader는 `CompletionStage`/`Mono`로 deadline/cancellation을 전달한다. null 의미는 하나로
정하고 requested key set/cardinality를 검증한다.
**필수 E2E**
- 50개 parent/child query의 fake application port 호출이 1회 또는 bounded chunk 수.
- request 간 cache 비공유, 같은 request duplicate key dedupe, actor/tenant scope 분리.
- never-completing loader timeout/cancel, 첫 chunk budget 소진 뒤 다음 chunk 0회.
- missing/failed/null/wrong-key map 계약.
### GQL-017 — MVC는 bounded라고 설명하지만 concurrency/queue가 unbounded이고 context도 전달하지 않는다
**근거**
- virtual-thread-per-task executor는 task admission을 제한하지 않는다.
- fixed thread pool은 기본 unbounded `LinkedBlockingQueue`를 사용한다.
- MVC adapter가 submit한 task를 `GraphQlContextPropagator.wrap`으로 감싸지 않는다.
- WebFlux blocking fallback은 `subscribeOn`이 없어 subscriber/event-loop thread에서 실행될 수 있다.
**구현 결정**
- executor 앞에 semaphore/bulkhead 또는 bounded `ThreadPoolExecutor` queue/rejection을 둔다.
- Spring GraphQL annotated controller executor를 canonical하게 구성해 double scheduling/wait을 피한다.
- context는 ThreadLocal만 믿지 말고 GraphQLContext/Reactor Context를 SSOT로 삼고 blocking bridge에서만
snapshot/wrap한다.
- timeout은 interrupt가 아니라 downstream deadline propagation과 함께 검증한다.
### GQL-018 — pipeline stage 순서는 authorization에 필요한 정보를 만들기 전에 authorize한다
**근거**
pipeline은 `AUTHORIZATION``PARSE_VALIDATE`보다 앞에 두지만 field authorization은 schema coordinate와
selected operation을 필요로 한다. 현재 pipeline이 실행되지 않아 장애는 잠복해 있지만 그대로 wiring할
수 없는 순서다.
**구현 결정: 실제 Chain of Responsibility**
```text
authenticate transport principal
→ create request context
→ persisted lookup / raw document admission
→ parse + validate + select operation
→ document/coordinate authorization
→ structural + complexity budget
→ execute + field/object authorization + DataLoader
→ map errors + observe + cleanup
```
각 handler는 입력 상태와 산출 상태를 typed record로 표현하고, 필요한 이전 stage가 없으면 compile-time 또는
startup validation에서 실패하게 한다.
### GQL-019 — subscription/replay/drain policy는 concurrent runtime state machine이 아니다
**근거**
- replay cursor는 expected subscription과 tenant를 검증하지 않는다.
- subscription event byte estimate는 실제 serialized bytes가 아니라 `payload.toString()`을 사용한다.
- drain coordinator는 draining check와 registration increment 사이 race가 있고, state publication 순서에
따라 startedAt을 null로 볼 수 있다.
- cancellation/listener collections와 protocol lifecycle의 thread-safety/ownership이 명시되지 않았다.
- WebSocket/SSE/RSocket “handler factory”는 실제 Spring transport handler가 아니라 policy 객체를 반환한다.
**구현 결정**
atomic immutable state 또는 lock-protected State pattern으로 `ACCEPTING→DRAINING→CLOSED`를 모델링한다.
registration은 lease를 받아 close 시 release한다. replay cursor는 actor+tenant+subscription profile에
bind한다. queue byte bound는 실제 serializer 결과로 계산한다. 실제 handler가 없으면 factory 명명과
지원 등급을 policy/catalog로 낮춘다.
### GQL-020 — preparsed cache의 expiry policy가 사용되지 않고 unrelated miss가 직렬화된다
`GraphQlPreparsedCachePolicy`의 expire-after-access 값은 provider에서 사용되지 않는다. cache miss parse가
synchronized block 안에서 실행되어 서로 다른 document도 직렬화된다. injected Clock/Ticker를 쓰는 bounded
cache와 per-key single-flight를 적용하고 expiry/access-refresh/same-key-once/different-key-parallel을 테스트한다.
### GQL-021 — cancellation hook 하나의 실패가 나머지 cleanup을 막는다
request/subscription cancellation listener loop가 exception을 aggregate하지 않는다. 세 hook 중 두 번째가
throw해도 세 개 모두 정확히 한 번 실행하고 첫 실패에 나머지를 suppressed로 붙이는 공통 cancellation
primitive로 합친다. 이미 `GraphQlContextCleanup`이 가진 all-cleanups 실행 의미를 재사용한다.
### GQL-022 — scalar input/output limit이 대칭이 아니고 작은 입력이 큰 출력을 만들 수 있다
- BigDecimal은 precision/scale/exponent/serialized length 제한 없이 parse 후 `toPlainString()`을 사용한다.
작은 `1E+1000000`이 매우 큰 output allocation을 만들 수 있다.
- custom Long scalar는 parse에 configured min/max를 적용하지만 serialize/valueToLiteral은 그 범위를 무시한다.
lexical length, precision, absolute scale, output length를 먼저 제한하고 Long의 input/output에 같은 range를
적용한다. coercion error에는 raw input을 포함하지 않는 기존 원칙을 유지한다.
### GQL-023 — operation name을 low-cardinality tag라고 가정할 수 없다
operation name은 길이/문법만 제한되어 client가 매번 임의 이름을 만들 수 있고 observation convention은 raw
name을 tag로 사용한다. 실제 Micrometer/Spring Observation interface 연결도 없다. persisted/registered
operation만 이름 tag로 사용하고 나머지는 `other`로 collapse하거나 production에서 anonymous/unregistered
operation을 거부한다. 10,000개 임의 name을 actual MeterRegistry에 넣어 series bound를 검증한다.
### GQL-024 — production jar가 testkit, fixed secret, in-memory development 구현을 함께 배포한다
main source에는 `testkit` 12개 class, `GraphQlConnectionAssembler.forTests()`의 fixed signing secret,
`GraphQlAuthenticationContextFactory.testContext`, test error context, in-memory persisted registry가 있다.
`java-test-fixtures` 또는 별도 `graphql-testkit` leaf로 옮기고 production jar에 `.testkit.`, `forTests`,
`testContext`, fixed secret이 없는 jar content gate를 둔다.
### GQL-025 — 373개 public 중심 type과 Stable/Advanced/testkit/release의 한 jar 결합은 변경 비용이 크다
package import graph에 명백한 cycle이 없는 방향성은 좋지만 package만으로 외부 API와 classpath isolation을
보장하지 못한다. 이번 ignored boundary package가 그 취약성을 실제로 보여 줬다. package-private를 default로
하고 explicit `api`/`spi`만 public으로 허용하는 API surface snapshot을 둔다. Gradle 분리는 28개를 한 번에
늘리지 않고 §7의 6~8개 capability 단위로 진행한다.
### GQL-026 — GraphQL request context와 storage SPI가 inbound에 있어 downstream 구현 방향과 충돌한다
문서는 `GraphQlRequestContext`/deadline을 application/JPA/Mongo/HTTP client까지 전달하고 persisted registry를
외부 durable store가 구현한다고 설명한다. application/outbound가 inbound leaf type을 구현하면 의존 방향이
뒤집힌다.
- GraphQL context는 inbound-local로 유지하고 application command의 actor/tenant/deadline 값으로 명시 매핑한다.
- object authorization은 application-core의 transport-neutral use case로 두고 GraphQL bridge가 호출한다.
- persisted operation 저장은 generic operational store/cache port를 neutral contract owner에 두고 GraphQL
adapter가 key/value mapping만 소유한다. inbound→outbound 직접 edge는 만들지 않는다.
- composition root는 연결만 하고 business/storage policy를 소유하지 않는다.
### GQL-027 — 문서와 실제 설정·테스트·지원 등급이 drift했다
- README는 custom `@ConfigurationProperties`가 없다고 하지만 `backend.graphql` properties가 있다.
- build comment는 `spring.graphql.platform.*`를 언급하지만 실제 prefix는 `backend.graphql`이다.
- CLAUDE/README는 누락된 boundary classes/tests가 있다고 기록한다.
- “더 이상 미구현이 아니다”라는 표현은 policy object 존재와 runtime integration을 구분하지 않는다.
- test count는 compile이 깨진 현재 실행 증거가 아니라 과거/문서 count다.
generated configuration metadata, actual bean inventory, random-port adoption test, task JUnit XML에서 문서를
생성/검증한다. capability마다 `modelled`, `wired`, `integration-verified`, `production-verified`를 분리하고
현재 수준 이상으로 표현하지 않는다.
## 6. 디자인 패턴 적용 제안
### 6.1 적용할 패턴
| 위치 | 패턴 | 적용 형태 | 해결하는 문제 |
|---|---|---|---|
| 실행 pipeline | Chain of Responsibility | typed stage handler + actual Spring execution decorator | stage 목록만 있고 실행되지 않는 문제 |
| Spring integration | Adapter | pure policy를 interceptor/instrumentation/wiring으로 변환 | framework-free core와 runtime 연결 분리 |
| transport | Strategy | MVC/WebFlux leaf별 transport strategy | 두 runtime classpath와 blocking policy 혼합 제거 |
| DataLoader | Decorator | loader에 chunk/deadline/auth/observation을 조합 | 병렬 custom framework와 정책 중복 제거 |
| error | Mapper + Adapter | pure wire-error mapper + Spring resolver | 두 resolver/category drift 제거 |
| cursor | Versioned Codec Strategy | v1 read/v2 write codec과 key-ring signer | framing migration과 rotation 분리 |
| persisted/admin/subscription | State | 허용 transition과 CAS version 명시 | blocked 재활성, drain race, 허위 audit 제거 |
| application 경계 | Anti-Corruption Mapper | GraphQL context/input → command/context | transport DTO/application leakage 방지 |
| configuration | Validated Plan/Builder | bind → aggregate validate → immutable runtime plan | resource 생성 뒤 validation과 inert setting 제거 |
### 6.2 피할 패턴
- `Factory`, `Generator`, `Interceptor`라는 이름만 붙이고 metadata/policy 객체만 반환하지 않는다.
- 28개 설계상 “모듈”을 근거 없이 28개 Gradle leaf로 기계 분해하지 않는다.
- controller/router와 Spring 기본 endpoint를 병렬로 유지하지 않는다.
- custom DataLoader, custom preparsed cache, custom transport를 framework가 제공하는 extension point와 경쟁시키지 않는다.
- architecture rule을 runtime reflection suffix 검사 하나로만 강제하지 않는다.
- Advanced라는 이유로 repository/use-case 경계를 완화하지 않는다.
## 7. 권장 Gradle·폴더 구조
### 7.1 대안 비교
| 대안 | 장점 | 단점 | 판정 |
|---|---|---|---|
| A. 현재 단일 leaf 유지 + 경계 test 복구 | 가장 빠름, registry 변경 최소 | public/classpath/runtime 결합 유지 | GQL-001 응급 복구용 |
| B. 6~8 capability leaf로 단계 분리 | 실제 runtime 책임과 dependency를 격리 | registry/settings/lock/CI 갱신 필요 | **권장** |
| C. 설계의 28 package를 28 leaf로 분리 | 가장 강한 compile boundary | Gradle/lock/CI 비용과 빈 facade 증가 | 현재 과도함 |
### 7.2 권장 target
```text
graphql-platform-core
src/main/java/.../graphql/core/api
src/main/java/.../graphql/core/policy
# pure Java, framework/transport/application type 없음
graphql-schema
src/main/java/.../graphql/schema
src/main/java/.../graphql/scalar
src/main/java/.../graphql/compat
# GraphQL Java AST/wiring, no web server
graphql-spring-execution
src/main/java/.../graphql/execution
src/main/java/.../graphql/security
src/main/java/.../graphql/error
src/main/java/.../graphql/dataloader
src/main/java/.../graphql/autoconfigure
# application-core bridge + Spring GraphQL extension points
graphql-transport-mvc
src/main/java/.../graphql/http/mvc
# starter-web only
graphql-transport-webflux
src/main/java/.../graphql/http/webflux
# starter-webflux only
graphql-advanced
src/main/java/.../graphql/advanced/{persisted,subscription,federation,...}
# 실제 wired capability만 opt-in; feature가 커지면 사용 단위별 추가 분리
graphql-testkit
src/testFixtures/java 또는 전용 leaf
graphql-release-verification
# Gradle/build logic와 evidence manifest, production runtime에 포함하지 않음
```
허용 방향의 기본안은 다음과 같다.
```text
transport-mvc/webflux → spring-execution → schema → platform-core
spring-execution → application-core → domain-core
advanced → spring-execution/schema/platform-core
testkit → 공개 api/spi만
release-verification → 각 leaf의 test/evidence artifact만
```
실제 edge와 runtime membership은 반드시 `src/config/architecture/modules.json`에 먼저 등록하고 같은 SSOT의
Gradle gate로 검증한다. `graphql-persisted-<database>`가 inbound contract에 역의존하는 구조는 만들지 않는다.
### 7.3 package visibility
- public 허용: 외부 resolver/adopter가 구현·호출해야 하는 `api`, `spi`, configuration properties.
- package-private/internal: calculator, parser walker, state transition, mapper implementation, factory implementation.
- test-only: fixture, fake/in-memory, fixed key/principal/context, contract assertion helper.
- public API snapshot에는 FQCN, constructor/method signature, stability level을 기록한다.
## 8. 구현 순서 — 그대로 issue/PR로 분리 가능한 단위
### Wave 0 — build와 증거 복구
1. **PR GQL-001A**: ignored package red test와 `moduleboundary` package 복구.
2. **PR GQL-001B**: module boundary negative fixtures와 required FQCN/lane 연결.
3. focused test, Stable/contract/Advanced lane을 실행한다. 여기서 발견되는 test failure는 다음 wave의
characterization backlog로 분리한다.
### Wave 1 — 실제 endpoint baseline
1. 현재 기본 `/graphql`에 health query를 보내는 full configuration test를 만든다.
2. cost/auth/DataLoader/custom adapter bean이 존재하지만 호출되지 않는 현재 상태를 failing test로 증명한다.
3. Spring-native endpoint를 canonical로 확정하고 custom transport dead path를 제거한다.
4. auto-configuration imports, binder defaults, framework property cross-check를 추가한다.
### Wave 2 — executable pipeline
1. request context interceptor.
2. parse/select/introspection/cost/auth instrumentation/decorator.
3. scalar/preparsed/DataLoader/error wiring.
4. actual observation과 cleanup/cancellation.
5. servlet random-port qualification을 platform adoption test로 교체한다.
### Wave 3 — correctness/security
서로 독립인 작은 PR로 다음을 처리한다.
- null-preserving request JSON + byte/nesting limit.
- Accept negotiation.
- fragment introspection/selected-operation analyzer.
- cursor v2/rotation/direction/tenant.
- mutation canonical fingerprint/tenant.
- schema kind/default/extensions/directives.
- scalar bounds.
- persisted admin/subscription state machines.
각 PR은 먼저 failing unit/property/integration test를 추가한다.
### Wave 4 — architecture와 모듈 분리
1. actual controller generic/bytecode gate와 application import gate.
2. repository exposure capability 제거.
3. testkit/fixed/in-memory API를 test fixtures로 이동.
4. `platform-core`, `schema`, `spring-execution` 추출.
5. MVC/WebFlux leaf 분리와 runtime classpath tests.
6. Advanced/release verification을 runtime jar에서 분리.
7. public API snapshot과 package-private 축소.
### Wave 5 — Advanced promotion
각 capability는 다음 네 증거가 모두 있을 때만 `wired` 이상으로 승격한다.
1. 실제 Spring handler/extension point가 존재한다.
2. real request 또는 protocol-level integration test가 해당 path를 호출한다.
3. disabled 상태에서 bean/resource/route가 0개다.
4. restart/concurrency/fault가 필요한 stateful capability는 durable evidence가 있다.
codegen/federation/subscription/persisted operation이 이 기준을 못 채우면 policy/catalog로 이름과 문서를
낮추고 production support claim을 하지 않는다. Spring GraphQL은 federation에 `@EntityMapping`을 포함한
공식 통합을 제공하므로 별도 facade보다 이를 우선 검토한다
([Spring GraphQL federation](https://docs.spring.io/spring-graphql/reference/federation.html)).
## 9. 테스트 전략과 Definition of Done
### 9.1 최소 테스트 피라미드
| 계층 | 테스트 | 핵심 assertion |
|---|---|---|
| pure policy | unit + property | canonicalization, bounds, transition, deterministic output |
| Spring composition | `ApplicationContextRunner` | enabled/disabled, bean exact set, unsafe config failure |
| schema/execution | `ExecutionGraphQlServiceTester` | scalar, parse, validation, error path, DataLoader |
| transport | random-port MVC/WebFlux | media type, auth, body cap, actual policy rejection |
| architecture | ArchUnit/bytecode + Gradle edge | generic DTO/entity/repository, package/leaf edge, public API |
| stateful Advanced | concurrency/restart/store integration | CAS/fencing/audit/replay/durable transition |
| release | same-SHA evidence manifest | 실행한 lane/version/scenario와 지원 문서 일치 |
### 9.2 전체 완료 조건
- `:adapter:inbound:graphql:compileJava`, focused `test`, Stable/contract/Advanced lane이 실행되고 green이다.
- performance lane이 필요한 support claim은 실제 tagged scenario/evidence 없이는 승격되지 않는다.
- real `/graphql` E2E에서 모든 mandatory policy가 최소 한 번 차단/허용 경로를 가진다.
- GraphQL DTO/context/framework type이 application/domain에 유출되지 않는다.
- controller/resolver가 repository, persistence entity, transaction을 직접 소유하지 않는다.
- MVC/WebFlux runtime dependency가 서로의 server stack을 끌어오지 않는다.
- production jar에 testkit/fixed secret/in-memory development facade가 없다.
- public API와 capability support status가 snapshot/manifest로 검증된다.
- docs의 property name/test count/support status는 generated metadata와 JUnit evidence에서 파생된다.
## 10. 이번 리뷰에서 실행한 검증
### 성공
```bash
cd src
./gradlew verifyCleanArchitectureDependencies --console=plain
```
- current HEAD fresh run은 `BUILD SUCCESSFUL in 769ms`, 1 actionable task executed였다.
- 이 결과는 registry에 선언된 project dependency edge가 맞다는 증거다.
- 누락된 내부 package boundary, runtime wiring, correctness를 승인하는 증거는 아니다.
### 실패
```bash
cd src
./gradlew :adapter:inbound:graphql:compileJava --console=plain
./gradlew :adapter:inbound:graphql:test --console=plain
```
- direct `compileJava`의 current HEAD fresh 재현은 `BUILD FAILED in 1s`, 7 errors였다
(직전 첫 재현도 같은 7 errors, 7s).
- focused `test`도 같은 `compileJava` 단계에서 실패했다.
- 누락 package: `dev.caskeleton.adapter.inbound.graphql.build`.
- 참조 파일: `GraphQlPlatformAutoConfiguration`, `GraphQlAdvancedDependencyRules`.
- test 75개는 실행 단계에 진입하지 못했다.
### 정적 재현
```bash
git check-ignore -v --no-index \
src/adapter/inbound/graphql/src/main/java/dev/caskeleton/adapter/inbound/graphql/build/GraphQlBuildModel.java
```
- `src/.gitignore:2:build/`이 반환되어 누락 source package와 ignore rule의 충돌을 확인했다.
### 미실행
- `graphqlStableTest`, `graphqlContractTest`, `graphqlAdvancedTest`: 동일 compile blocker 때문에 실행 불가.
- `graphqlPerformanceTest`: compile blocker에 더해 실제 tagged load/fault scenario가 없는 상태.
- repository 전체 `test`/`check`: review-only 범위이며 focused compile blocker가 먼저 존재한다.
- production adopter, actual feature schema, JPA/Mongo query-count, real WebSocket/SSE/RSocket, load/soak/fault.
## 11. 남은 위험과 판정 범위
- GraphQL leaf는 현재 두 composition root runtime에 포함되지 않으므로 발견 사항을 현 서비스의 즉시 runtime
장애로 확대하지 않는다.
- 반대로 빈 runtime membership은 adopter 안전성의 증거도 아니다. opt-in 직후 compile/auto-config/runtime
wiring 문제가 드러난다.
- build blocker가 해결되면 지금까지 실행되지 못한 524 test annotation에서 추가 failure가 나올 수 있다.
- Advanced 130개 production class의 모든 concurrent/protocol path를 실환경에서 검증하지 않았다.
- 공식 Spring GraphQL extension point 선택은 타당하지만 정확한 Boot 4.0.0/Spring GraphQL 2.0.0 API
signature와 auto-configuration ordering은 구현 시 lock 기준으로 확인해야 한다.
- 이 리뷰의 `FACT`는 명시한 source/command에 한정되고, target module split은 그 사실에서 도출한
`INFERENCE/권고`다. registry 변경 전에 별도 설계 문서와 실행 계획을 남겨야 한다.
최종 판정은 **CHANGES REQUIRED**다. 구현 순서는 `GQL-001 → GQL-002/003 → GQL-005~018 →
GQL-024~026 → Advanced promotion`을 권장한다.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff