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이 되더라도 “정책 객체가 맞다”는 증거와
“실제 요청에 정책이 강제된다”는 증거를 분리해야 한다.
즉시 적용할 원칙은 다음과 같다.
- GQL-001을 단독 PR로 먼저 처리해 compile과 내부 경계 검사를 복구한다.
- 복구 전후 모두 현재 artifact를
runtime-ready GraphQL execution platform으로 승격하지 않는다. - Spring 기본
/graphql을 canonical transport로 정하고, 정책을 공식 extension point에 연결한다. - 자체 MVC/WebFlux adapter를 실제 endpoint로 만들 계획이 없다면 제거한다. 평행 실행 경로를 두지 않는다.
- repository 자동 노출은 Advanced라도 제거한다. application use case를 우회하는 예외를 만들지 않는다.
- correctness/security red test를 먼저 고정한 뒤 public API와 Gradle leaf를 단계적으로 분리한다.
- 실제 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이 된다.
구현 결정
- source package 이름을
...graphql.build가 아니라...graphql.moduleboundary로 바꾼다..gitignore예외보다 역할이 분명하고 다른 도구의build디렉터리 규칙과 충돌하지 않는다. - 세 production model을 복원한다. 단, source-tree scanner가 runtime에 필요하지 않으면
GraphQlBuildModel을 test/build logic으로 이동하고GraphQlPlatformAutoConfiguration의 runtime source scan을 제거한다. GraphQlModuleBoundaryTest는 실제 source/import graph를 검사하며 다음 세 negative fixture를 가진다. Stable→Advanced import, core package의 Spring/GraphQL/Reactor import, 등록되지 않은 package.graphqlStableTest가 해당 FQCN을 명시적으로 포함하고, test가 0개면 실패하게 유지한다.- 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 JavaInstrumentation, SpringBatchLoaderRegistry, 실제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 단일 실행 경로
- Spring 기본
/graphql을 canonical HTTP transport로 유지한다. GraphQlPlatformWebInterceptor implements WebGraphQlInterceptor에서 인증 principal을 검증된 request context로 매핑하고 GraphQL/Reactor context에 넣는다.GraphQlPlatformInstrumentation또는ExecutionGraphQlServicedecorator에서 document parse/selection, introspection, authorization, cost, deadline/cancellation을 실행한다.RuntimeWiringConfigurer,BatchLoaderRegistry등록/decorator, actual preparsed document provider, canonical exception resolver를 auto-configuration이 bean으로 조립한다.- 현재 MVC/WebFlux custom adapters는 제거한다. 자체 transport가 반드시 필요하다면 Spring 기본 handler를 끄고 실제 route를 소유하게 하며, 두 경로를 동시에 두지 않는다.
- 모든 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,
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 한 개뿐이다.GraphQlPlatformPropertiesprimitive 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()상수를 검증한다.
구현 결정
- 재사용 starter라면
@AutoConfiguration(after = GraphQlAutoConfiguration.class)과META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports를 추가한다. 내부 composition 전용이면 이름을GraphQlPlatformConfiguration으로 바꾸고 app-bootstrap에서 명시 import한다. - properties를 nested record/class로 나누고 binder가 실제로 사용하는 default를 선언한다.
- framework
GraphQlProperties를 SSOT로 삼거나 custom flag와의 불일치를 startup failure로 만든다. - startup validator는 실제 주입된 pipeline, scalar manifest, client policies, key ring을 검증한다.
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도 남는다.
구현 결정
- servlet filter/reactive web filter 또는 bounded decoder에서 raw HTTP body byte cap을 먼저 적용한다.
- JSON decode 후 query/variables/extensions를 UTF-8 byte 기준으로 검증한다.
- variables/extensions는 null-preserving deep immutable JSON value copy를 사용한다.
- 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).
구현 결정
- build-time에는 ArchUnit/bytecode scan으로 실제 configured controller packages를 재귀 검사한다.
- runtime에는 ApplicationContext의 실제 GraphQL controller bean/method를 startup 검사한다.
- Java
Type을 재귀 순회해 parameterized/array/wildcard/type-variable bound를 본다. - Query/Mutation에는 single-value async(
Mono,CompletionStage)를 허용하고 multi-value publisher만 Subscription에 제한한다. - 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
- v2 payload를 canonical JSON/CBOR 또는 length-prefixed typed framing으로 만든다.
- codec이 active key id를 선택하고 envelope에 기록한다. caller payload가 signing key를 선택하지 않는다.
- decode 입력에 expected query/filter/direction/tenant-scope fingerprint를 포함한다.
- v1 decode를 migration 기간에만 유지하고 v2만 발급한다.
- key ring map/key set을 완전 불변으로 만들고 secret clone은 유지한다.
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만 실제 SpringDataFetcherExceptionResolverAdapter와@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/DEPRECATEDaudit을 남긴다. remove()는 실제 삭제가 아니라 BLOCKED 전환이다.- BLOCKED에서 DEPRECATED로 바꿔 다시 executable하게 만들 수 있는 transition guard가 없다.
- raw operator 문자열 allowlist를 받고 credential kind 거부 메서드는 service가 호출하지 않는다.
- registry 변경과 in-memory
ArrayListaudit은 원자적이지 않고 thread-safe하지 않다.
구현 결정: State + authenticated command + durable transaction
- transport가 만든
GraphQlAdminPrincipal만 service에 전달한다. - lifecycle transition table을 두고 BLOCKED는 explicit audited unblock 전까지 terminal로 취급한다.
- registry command는 updated record/version을 반환하거나 not-found/conflict를 던진다.
- mutation과 audit append를 하나의 durable transactional port로 묶는다.
- 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 변화는 오탐할 수 있다.
구현 결정
- registry를 extension까지 normalize하거나 executable schema로 compile한 canonical model을 비교한다.
TYPE_KIND_CHANGED,INPUT_DEFAULT_REMOVED/CHANGED/ADDED, applied-directive change를 명시한다.- nested list/non-null 변화는 input/output position별 방향성을 재귀 분류한다.
- scalar coercion compatibility는 SDL이 아니라 scalar manifest codec/version 계약으로 분리한다.
GQL-016 — custom DataLoader contract는 실제 N+1과 timeout을 보장하지 않는다
근거
GraphQlDataLoaderRequestRegistry는Objectmap이며 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
ThreadPoolExecutorqueue/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
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.graphqlproperties가 있다. - 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와 증거 복구
- PR GQL-001A: ignored package red test와
moduleboundarypackage 복구. - PR GQL-001B: module boundary negative fixtures와 required FQCN/lane 연결.
- focused test, Stable/contract/Advanced lane을 실행한다. 여기서 발견되는 test failure는 다음 wave의 characterization backlog로 분리한다.
Wave 1 — 실제 endpoint baseline
- 현재 기본
/graphql에 health query를 보내는 full configuration test를 만든다. - cost/auth/DataLoader/custom adapter bean이 존재하지만 호출되지 않는 현재 상태를 failing test로 증명한다.
- Spring-native endpoint를 canonical로 확정하고 custom transport dead path를 제거한다.
- auto-configuration imports, binder defaults, framework property cross-check를 추가한다.
Wave 2 — executable pipeline
- request context interceptor.
- parse/select/introspection/cost/auth instrumentation/decorator.
- scalar/preparsed/DataLoader/error wiring.
- actual observation과 cleanup/cancellation.
- 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와 모듈 분리
- actual controller generic/bytecode gate와 application import gate.
- repository exposure capability 제거.
- testkit/fixed/in-memory API를 test fixtures로 이동.
platform-core,schema,spring-execution추출.- MVC/WebFlux leaf 분리와 runtime classpath tests.
- Advanced/release verification을 runtime jar에서 분리.
- public API snapshot과 package-private 축소.
Wave 5 — Advanced promotion
각 capability는 다음 네 증거가 모두 있을 때만 wired 이상으로 승격한다.
- 실제 Spring handler/extension point가 존재한다.
- real request 또는 protocol-level integration test가 해당 path를 호출한다.
- disabled 상태에서 bean/resource/route가 0개다.
- 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, focusedtest, Stable/contract/Advanced lane이 실행되고 green이다.- performance lane이 필요한 support claim은 실제 tagged scenario/evidence 없이는 승격되지 않는다.
- real
/graphqlE2E에서 모든 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을 권장한다.