Files
clean-architecture-backend-…/docs/reviews/2026-08-14-graphql-module-code-review.md

54 KiB

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.javasrc/.gitignore:2:build/를 반환한다.
  • GraphQlPlatformAutoConfiguration.java:3,107-108advanced/bootstrap/GraphQlAdvancedDependencyRules.java:3,26-28,43은 존재하지 않는 dev...graphql.build.GraphQlBuildModel을 참조한다.
  • CLAUDE.md:36-40, README.md:108-115, graphql-platform-conventions.gradle:11-13GraphQlStableModule, GraphQlAdvancedModule, GraphQlBuildModel, GraphQlModuleBoundaryTest가 실제 tree를 검사한다고 기록하지만 네 파일은 main/test tree에 없다.
  • focused :testcompileJava에서 해당 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 검사를 함께 둔다.

필수 테스트/검증

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는 GraphQlExecutionRequestGraphQlExecutionContext를 입력·출력하는 실행 가능한 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, RuntimeWiringConfigurer, BatchLoaderRegistry다. 구현 시 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-20spring-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).

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을 합산한다.

실패 입력

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).

구현 결정

  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/GraphQlRepositoryExposureValidatorGraphQlRepositoryAllowlist는 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가 의도면 removeretireAndBlock으로 이름 바꾼다.

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 Fooinput 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을 보장하지 않는다

근거

  • GraphQlDataLoaderRequestRegistryObject 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은 AUTHORIZATIONPARSE_VALIDATE보다 앞에 두지만 field authorization은 schema coordinate와 selected operation을 필요로 한다. 현재 pipeline이 실행되지 않아 장애는 잠복해 있지만 그대로 wiring할 수 없는 순서다.

구현 결정: 실제 Chain of Responsibility

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

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에 포함하지 않음

허용 방향의 기본안은 다음과 같다.

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).

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. 이번 리뷰에서 실행한 검증

성공

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를 승인하는 증거는 아니다.

실패

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개는 실행 단계에 진입하지 못했다.

정적 재현

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을 권장한다.