# GraphQL API 실행 플랫폼 설계서 - **문서 상태:** 구현 기준선 확정 - **기준일:** 2026-08-12 - **대상 저장소:** Java/Spring Backend Skeleton - **Root package:** `io.backend.skeleton.graphql` - **Stable module root:** `modules/graphql` - **Advanced module root:** `modules/graphql-advanced` - **요구사항 원본:** `GraphQL API 실행 플랫폼 심층 리서치` ## 0. 확정 경계 요약 - Stable 기본 토폴로지는 **Single Executable Schema**다. - **JPA Entity와 MongoDB Document**는 GraphQL Input·Output 계약으로 직접 노출하지 않는다. - **GraphQL Multipart Upload**는 지원하지 않으며 binary lifecycle은 Fileserver가 소유한다. - Validation 이후 execution 중 Field Error가 발생해도 가능한 Partial Data는 HTTP `200`으로 반환한다. - 여러 Mutation Root Field를 하나의 request-wide database transaction으로 묶지 않는다. ## 1. 문서 목적 이 문서는 GraphQL Java와 Spring for GraphQL의 편의 Wrapper를 만드는 문서가 아니다. **SDL로 정의된 외부 API 계약이 인증, 요청 제한, parse·validation, operation 정책, 비용 판정, resolver, DataLoader, Application Use Case, partial data와 error, 실시간 stream으로 실행되는 전 과정**을 통제하는 플랫폼의 설계 기준을 확정한다. 구현자가 다시 결정하지 않도록 다음 항목을 명시적으로 고정한다. ```text Schema 소유권과 조립 방식 Transport 지원 범위 Request Context Resolver와 Application Service 경계 Blocking·Reactive 실행 Profile DataLoader 요청 범위와 Batch 계약 Selection Set과 Fetch Profile Cursor 형식과 서명 Mutation·Transaction·Idempotency 의미 Error wire contract Field·Object·Tenant 권한 Query cost와 abuse control Preparsed cache와 Persisted Operation 경계 Subscription의 delivery 한계 Schema compatibility와 Release Gate ``` ## 2. 핵심 결론 ```text GraphQL Platform = Schema Contract + Transport Profile + Execution Policy + Governance + Security + Verification GraphQL Platform ≠ Database Gateway ≠ JPA/Mongo Repository 자동 노출기 ≠ Durable Messaging Broker ≠ Binary Upload Server ≠ Request-wide Database Transaction Manager ``` 플랫폼은 GraphQL operation을 Application Use Case에 연결한다. 저장소와 외부 시스템의 고유 의미론은 기존 JPA, MongoDB, HTTP Client, Messaging, Fileserver, Object Storage 모듈이 계속 소유한다. ### 2.1 GraphQL 플랫폼이 소유한다 - SDL resource discovery, assembly, validation, fingerprint - Schema 변경 호환성 정책과 deprecation removal gate - HTTP, WebSocket, SSE transport profile - immutable `GraphQlRequestContext` - client·operation policy manifest - parser, depth, field, alias, fragment, complexity, response budget - resolver return/input type와 application boundary - DataLoader request scope, batch, timeout, missing key, key error - Selection Set을 유한한 Fetch Profile로 분류하는 규칙 - signed cursor envelope - GraphQL error wire contract와 내부 오류 마스킹 - persisted operation registry와 operation block - subscription connection, auth, buffer, ordering 정책 - metric·trace cardinality policy - schema·transport·security·performance release gate ### 2.2 도메인·Application 모듈이 소유한다 - Query, Mutation, Subscription의 업무 의미 - GraphQL Input·Output DTO와 Read Model - Application Use Case - 도메인 상태가 필요한 authorization decision - transaction 요구 - 저장소 query, fetch, pagination 의미 - integration event의 업무 의미 - 예상 가능한 typed business result ### 2.3 기존 기술 모듈이 소유한다 | 영역 | 기존 모듈 책임 | GraphQL 책임 | |---|---|---| | JPA | Entity, Repository, Transaction, Fetch Plan, Lock | Fetch Profile을 선택해 Application Query 호출 | | MongoDB | Document, Query, Aggregation, Consistency | Fetch Profile을 선택해 Application Query 호출 | | HTTP Client | Timeout, Retry, TLS, SSRF, Bulkhead | Application 결과를 GraphQL DTO로 조립 | | Messaging | ACK, Replay, DLQ, durable event | Subscription source에 event 제공 | | Fileserver | Binary lifecycle, 검사, Range, upload | Upload reservation과 file metadata 반환 | | Object Storage | bytes, checksum, delegated access | 직접 호출하지 않음 | | WebSocket | 범용 연결 인프라 | GraphQL subscription protocol과 execution | ## 3. 지원 기준 | 구성 | 기준 | 등급 | |---|---|---| | Java | 21 | Stable | | Spring Boot | 프로젝트 4.1 BOM | Source of Truth | | Spring for GraphQL | 2.0 계열 | Stable | | GraphQL Java | Boot-managed v25 계열 | Stable | | GraphQL Specification | September 2025 | Contract | | GraphQL over HTTP | Stage 2 Draft | Versioned compatibility profile | | HTTP POST | MVC·WebFlux | Stable | | WebSocket | `graphql-transport-ws` | Advanced Stable | | SSE | Distinct Connection | Advanced | | RSocket | Spring Extension | Experimental | | Federation Subgraph | federation-jvm | Advanced | | HTTP GET | Draft compatibility | Experimental | | Multipart Upload | Fileserver 사용 | Unsupported | | HTTP array batch | Core 밖 | Unsupported | | Incremental Delivery | 별도 실험 | Experimental | Spring Boot BOM이 Spring for GraphQL과 GraphQL Java 조합의 기준이다. GraphQL Java 직접 override는 보안 대응이나 신규 기능 검증을 위한 별도 compatibility lane에서만 허용한다. ### 3.1 HTTP Draft 상태 코드 결정 Stable HTTP profile은 Spring for GraphQL 2.0 계열의 실제 동작을 계약으로 삼는다. ```text malformed JSON / parse / validation / coercion → application/graphql-response+json에서 4xx validation을 통과해 execution 시작 후 field error → HTTP 200 + data/errors partial data + errors → HTTP 200 ``` 이동 중인 GraphQL over HTTP Draft의 294 제안은 Stable에 선제 도입하지 않는다. Draft 변화는 별도 compatibility job에서 추적한다. ## 4. 공개 기능 계층 ```text G1 Standard GraphQL API - SDL - Query / Mutation - Annotated Controller - HTTP POST - Error Contract - Request Context - DataLoader - Cursor Connection - Cost Control G2 Advanced Execution - Persisted Operation - Registered Fetch Profile - WebSocket / SSE Subscription - DataLoader Chaining - Advanced Directive / Scalar G3 Extension - Federation Subgraph - RSocket - Code Generation - Spring Data Compatibility - HTTP Draft Compatibility - Incremental Delivery G4 Admin Plane - Schema Diff - Persisted Operation 등록·차단 - Schema Usage - Cost Profile - Subscription Runtime 진단 - Federation Composition ``` 일반 애플리케이션에는 `graphql.GraphQL`, 자유형 `GraphQLCodeRegistry`, raw `DataFetcher` registry를 공개하지 않는다. Infrastructure SPI 또는 명시적 G3 모듈에서만 사용한다. ## 5. 확정 모듈 구조 ```text modules/graphql/ ├── graphql-core-api ├── graphql-schema ├── graphql-execution ├── graphql-controller ├── graphql-http ├── graphql-dataloader ├── graphql-pagination ├── graphql-security ├── graphql-cost-control ├── graphql-error ├── graphql-observability ├── graphql-spring-boot-starter ├── graphql-testkit-core ├── graphql-testkit-schema ├── graphql-testkit-http └── graphql-testkit-integration modules/graphql-advanced/ ├── graphql-persisted-operation ├── graphql-admin ├── graphql-subscription ├── graphql-websocket ├── graphql-sse ├── graphql-federation ├── graphql-codegen ├── graphql-spring-data-compat ├── graphql-rsocket ├── graphql-http-draft-compat ├── graphql-incremental-experimental ├── graphql-testkit-realtime └── graphql-testkit-federation ``` ### 5.1 Stable dependency map ```text graphql-core-api → Java standard library only graphql-schema → core-api → GraphQL Java schema API graphql-execution → core-api → schema → Spring GraphQL execution API graphql-controller → core-api → execution → Spring annotated controller graphql-http → core-api → execution → Spring MVC / WebFlux graphql-dataloader / pagination / security / cost-control / error → core-api → execution graphql-observability → core-api → execution graphql-spring-boot-starter → every Stable runtime module → no Advanced module ``` ## 6. 중심 공개 계약 ```java public record GraphQlSchemaContract( String schemaHash, String breakingPolicyVersion, String scalarManifestVersion, String directiveManifestVersion) {} public record GraphQlRequestContext( ActorRef actor, TenantContext tenant, GraphQlClientProfile clientProfile, Locale locale, GraphQlOperationId operationId, String traceId, GraphQlDeadline deadline) {} public record GraphQlClientPolicy( int maxDocumentBytes, int maxVariablesBytes, int maxDepth, int maxFields, int maxAliases, int maxFragments, int maxInputListElements, int defaultPageSize, int maxPageSize, long maxComplexity, long maxResponseNodes, long maxResponseBytes, Duration maxExecutionTime, boolean introspectionAllowed, boolean persistedOperationOnly, boolean namedOperationRequired) {} public record GraphQlBatchPolicy( String loaderName, int maxBatchSize, Duration timeout, MissingKeyPolicy missingKeyPolicy, BatchErrorPolicy errorPolicy) {} ``` ```java public interface GraphQlFetchProfileRegistry { GraphQlFetchProfile select( GraphQlSchemaCoordinate coordinate, Set selectedFields, GraphQlClientProfile clientProfile); } public interface GraphQlCursorCodec { String encode(GraphQlCursorEnvelope cursor); GraphQlCursorEnvelope decode(String encoded); } public interface GraphQlErrorContract { GraphQlWireError map(Throwable failure, GraphQlErrorContext context); } ``` ## 7. Schema 계약 ### 7.1 SDL First SDL이 외부 API 계약의 Source of Truth다. 도메인 모듈이 자기 schema fragment를 소유하고 플랫폼이 조립·검증한다. ```text modules/order/src/main/resources/graphql/order/ ├── order-type.graphqls ├── order-query.graphqls └── order-mutation.graphqls modules/graphql/graphql-schema/src/main/resources/graphql/common/ ├── scalar.graphqls ├── directive.graphqls ├── connection.graphqls └── error.graphqls ``` ### 7.2 Input·Output·Persistence 분리 - JPA Entity와 Mongo Document를 GraphQL output으로 반환하지 않는다. - GraphQL input을 Entity·Document에 직접 bind하지 않는다. - Provider SDK 객체와 자유형 `Map`를 wire contract로 쓰지 않는다. - generated type은 client 또는 transport DTO까지만 허용한다. ### 7.3 Nullability Non-null은 DB column의 `NOT NULL`이 아니다. **Resolver, authorization, dependency failure까지 포함해 항상 값을 제공한다는 API 보장**이다. - identity는 `ID!` 후보 - 외부 enrichment와 부분 실패 가능한 child는 nullable 우선 - 신규 field는 nullable로 도입하고 보장을 검증한 뒤 강화 - Non-null 변경은 schema diff와 data/resolver contract test 요구 - null propagation 경계를 golden response로 고정 ### 7.4 Scalar | Scalar | 정책 | |---|---| | ID | opaque string | | UUID | canonical string | | Instant | UTC ISO-8601 | | Date | ISO local date | | BigDecimal | precision-loss 없는 coercion | | Long | client numeric range 정책 | | URL | Advanced parser·normalization | | Email | format 검증만, ownership은 업무 | | JSON | allowlist된 coordinate만 | | Upload | 금지 | ### 7.5 `@oneOf` September 2025 규격의 `@oneOf`를 Stable 기능으로 지원한다. 정확히 하나의 nullable field가 non-null 값으로 제공되어야 하며 default value를 허용하지 않는다. Schema build와 coercion contract test를 Release Gate에 포함한다. ## 8. Schema assembly·검증·진화 Startup과 CI에서 다음 순서를 실행한다. ```text SDL resource discovery → parse → schema validation → duplicate type / field / directive → scalar / directive wiring → interface / union TypeResolver → SchemaMappingInspector → argument / nullability mapping → forbidden feature scan → compatibility diff → schema contract fingerprint ``` Stable profile에서는 unmapped field, unknown resolver, argument mismatch, nullability mismatch가 startup 실패다. ### 8.1 Compatibility 정책 - field 삭제·rename, required argument 추가, input 강화, output nullable 전환은 Breaking - output enum/union possible type 추가는 wire additive이나 generated-client review 요구 - scalar coercion 변경은 새 scalar/version - directive 의미 변경은 behavioral compatibility review - deprecated element 삭제 전 usage, persisted operation reference, support window, client owner 승인 확인 ```text @deprecated → Schema Usage Observation → Persisted Operation Reference Scan → Support Window → Client Owner Approval → Removal ``` ## 9. HTTP Transport Profile ### 9.1 Stable profile ```text Method: POST only Content-Type: application/json Accept: application/graphql-response+json preferred Legacy response: application/json compatible Request fields: query, operationName, variables, extensions ``` ### 9.2 요청 제한 - request bytes와 variable bytes를 GraphQL parse 전에 제한 - `variables`와 `extensions`는 object만 허용 - `extensions` key allowlist - Production named operation 필수 - Cookie 인증 profile은 CSRF 필수 - HTTP GET, array batch, multipart upload는 Stable 비지원 ### 9.3 파일 Upload ```text GraphQL Mutation → Fileserver upload reservation / ticket 생성 Client → Fileserver 또는 Object Storage로 binary 전송 GraphQL Query → file metadata와 상태 조회 ``` GraphQL `Upload` scalar, multipart parser, checksum, quarantine, Range download를 구현하지 않는다. ## 10. 실행 Profile ```text BLOCKING_MVC - JPA / blocking Mongo / blocking SDK - Java 21 virtual thread 또는 bounded executor REACTIVE_WEBFLUX - Reactive Mongo / WebClient / subscription - Reactor Context - event-loop blocking 금지 MIXED_CONTROLLED - 명시적 bridge와 executor/scheduler 전환 - 아무 반환형이나 자동 허용하지 않음 ``` Resolver catalog에는 `BLOCKING`, `ASYNC`, `REACTIVE`, `STREAM`을 등록한다. Runtime profile과 실행 유형이 충돌하면 startup 또는 architecture test에서 실패한다. Timeout은 분리한다. ```text transportHandshakeTimeout requestExecutionTimeout resolverBudget dataLoaderBatchTimeout subscriptionIdleTimeout subscriptionMaxAge shutdownDrainTimeout ``` GraphQL deadline은 JPA, MongoDB, HTTP Client의 하위 deadline에 전파한다. Timeout 이후 reactive publisher와 실제 resource가 취소되는지 검증한다. ## 11. Resolver와 Application Service 경계 일반 진입점은 `@QueryMapping`, `@MutationMapping`, `@SubscriptionMapping`, `@SchemaMapping`, `@BatchMapping`이다. Resolver가 수행한다. - GraphQL input을 application command/query로 변환 - Bean Validation - Actor·Tenant·Locale·Deadline 전달 - Application Use Case 호출 - GraphQL DTO·Payload·Connection 변환 Resolver가 수행하지 않는다. - `EntityManager`, `MongoTemplate` 직접 Query - HTTP retry·circuit breaker - 메시지 ACK·DLQ - Object Storage binary I/O - multi-step domain transition - provider exception 공개 Transaction annotation은 Resolver가 아니라 Application Service에 둔다. ## 12. Selection Set과 Fetch Profile Selection Set을 자유형 SQL/Mongo projection으로 변환하지 않는다. ```text Selection Set → SelectionClassifier → registered finite FetchProfile → Application Query → JPA/Mongo implementation ``` 예: ```text Order.BASIC Order.WITH_ITEMS Order.WITH_CUSTOMER Order.FULL_DETAIL ``` 실제 EntityGraph, DTO projection, JPQL, Native SQL, Mongo aggregation은 저장소 모듈이 소유한다. 등록되지 않은 조합은 명시적 fallback 또는 오류로 처리한다. ## 13. DataLoader 계약 - DataLoader instance와 cache는 GraphQL execution 요청 범위 - cross-request cache는 Redis/Application Cache로 분리 - Actor·Tenant·Locale·Deadline을 loader context에 전달 - batch size는 JPA IN, Mongo `$in`, downstream batch API 상한으로 제한 - ordered loader는 입력 key 순서를 유지 - mapped loader는 missing key 정책 명시 - key별 failure와 batch 전체 failure 분리 - timeout, cancel, metric을 loader 단위로 제공 - chained dispatch는 Advanced opt-in DataLoader는 root query의 과도한 Entity graph, Cartesian product, 잘못된 index, unbounded child collection을 해결하지 않는다. ## 14. Cursor Connection Cursor는 Base64만 적용한 JSON이 아니라 versioned authenticated envelope다. ```json { "v": 1, "profile": "orders-by-created-at", "direction": "FORWARD", "keyset": { "createdAt": "2026-08-12T01:00:00Z", "id": "..." }, "filter": "sha256:...", "kid": "cursor-key-2026-01", "mac": "..." } ``` - query profile, filter fingerprint, sort tie-breaker 고정 - HMAC key rotation - unknown version, signature mismatch, key mismatch, profile mismatch 거부 - page size를 decode 후 서버 정책으로 재검증 - `totalCount`는 opt-in resolver - JPA는 keyset/Scroll, MongoDB는 range+`_id`, 외부 API는 upstream cursor를 signed envelope 안에 보관 ## 15. Mutation·Transaction·Idempotency ```text Mutation root field serial execution ≠ request-wide DB transaction ``` 하나의 Mutation Resolver는 하나의 Application Use Case를 호출한다. 여러 변경이 원자적이어야 하면 하나의 명시적 Use Case Mutation을 제공한다. Optimistic version과 idempotency key는 Application command로 전달한다. ```text idempotency scope = actor/client identity + mutation coordinate + idempotency key + normalized input fingerprint ``` 동일 key와 다른 fingerprint는 conflict다. GraphQL transport는 DB retry, idempotency record, lock을 직접 구현하지 않는다. 예상 가능한 업무 분기는 typed payload/union으로 표현할 수 있다. 예기치 않은 dependency/internal failure는 GraphQL error로 남긴다. ## 16. Error wire contract ```text REQUEST_ERROR - malformed JSON - parse - validation - variable coercion FIELD_ERROR - resolver execution - partial data 가능 BUSINESS_RESULT - 예상 가능한 업무 결과 - typed payload/union 우선 INTERNAL_ERROR - 예상 밖 장애 - opaque message + executionId ``` 공개 `extensions` allowlist: ```text code category retryable executionId constraint safe logical field ``` 공개 금지: ```text Java exception class stack trace SQL / JPQL / Mongo query downstream URL과 provider body credential / token internal host raw tenant/user/object ID ``` Non-null propagation은 오류 계약과 함께 golden test로 고정한다. ## 17. Security ```text Transport Authentication → Client Profile Authorization → Operation Authorization → Field / Use Case Authorization → Object Authorization → Tenant Isolation ``` Schema visibility는 authorization을 대체하지 않는다. Tenant는 GraphQL argument가 아니라 인증 Context에서 결정한다. BatchLoader도 동일한 Actor·Tenant Context를 사용한다. 환경별 정책: | 환경 | Introspection | GraphiQL | |---|---|---| | Local | 허용 | 허용 | | Test | 허용 | 선택 | | Dev | 인증된 사용자 | 인증된 사용자 | | Staging | Admin/CI | 비활성 | | Prod internal | Client Profile 기준 | 비활성 | | Prod public | 제한 또는 비활성 | 비활성 | Introspection 차단만으로 보안을 완성하지 않는다. Field/Object authorization, cost limit, persisted operation, request size limit을 함께 적용한다. ## 18. Cost·DoS control 방어 순서: ```text HTTP body bytes → variables bytes → request envelope → parser character/token/grammar limits → parse → validation → operation count/name/type → introspection policy → depth/fields/aliases/fragments → input list/string → complexity → estimated response nodes → execution timeout → actual response bytes ``` Complexity는 cardinality와 resolver class를 포함한다. ```text field cost = base field cost + resolver weight + child cost × effective cardinality ``` Resolver catalog 예: | 유형 | 상대 비용 | |---|---:| | in-memory scalar/property | 1 | | indexed DB lookup | 2 | | batched relation | 3 | | bounded aggregation | 8 | | external batch API | 10 | | external per-object call | 20 | | search/heavy aggregation | 별도 승인 | 숫자는 표준값이 아니라 calibration 시작점이며 실제 latency, DB query, examined rows/documents, downstream call과 비교해 profile manifest에서 조정한다. ## 19. Cache와 Persisted Operation 경계 ```text DataLoader Cache → request internal data loading Preparsed Document Cache → parse/validation result Persisted Operation Registry → approved operation document Response Cache → Stable 초기 비지원 ``` Preparsed cache key: ```text documentHash schemaContractHash validationPolicyVersion clientSchemaProfile ``` Persisted Operation은 Advanced 모듈에서 다음을 소유한다. ```text operationId operationName sha256Document canonicalDocument schemaContractHash allowedClientProfiles maximumComplexity maximumVariablesBytes status: ACTIVE | DEPRECATED | BLOCKED ``` Incident 시 특정 operation을 application redeploy 없이 차단할 수 있어야 한다. ## 20. Subscription Subscription은 live response stream이며 durable messaging이 아니다. ```text Messaging → persistence / ACK / replay / retry / DLQ GraphQL Subscription → client selection / connected actor / authorization / live delivery / cancellation ``` 상태: ```text CONNECTING → AUTHENTICATING → READY → SUBSCRIBED → STREAMING → CANCELLING → COMPLETED AUTH_EXPIRED / SLOW_CONSUMER / SOURCE_FAILED / SERVER_DRAINING / PROTOCOL_ERROR ``` 정책: - `graphql-transport-ws` 사용 - connection-init timeout, max subscription, max connection age - credential expiry 시 connection 종료 - 민감한 profile은 event 전달 시 재인가 - bounded buffer - 기본 slow-consumer 정책은 연결 종료 - `LOW_LATENCY`와 `ORDERED` 분리 - replay는 표준 보장이 아니며 Messaging 기반 Advanced extension - SSE는 subscription-only Distinct Connection ## 21. Federation·Codegen·Spring Data Compatibility ### 21.1 Federation - 단일 executable schema가 Stable 기본 - Federation Subgraph는 독립 배포, 실제 schema ownership, composition CI, router owner, distributed trace, latency budget이 있을 때만 opt-in - Federation Router는 이 저장소 밖 - entity key는 owner, stability, deprecation policy 필요 - entity resolution은 request-scoped batch와 authorization 사용 ### 21.2 Code Generation 허용: ```text client request/response operation validation transport-only DTO ``` 금지: ```text Domain Entity Application Use Case Repository Persistence model ``` ### 21.3 Spring Data Compatibility `@GraphQlRepository` 자동 노출은 별도 compatibility 모듈에서 allowlist한다. Filter, sort, projection, pagination 정책을 명시하고 offset pagination 기본값을 조용히 채택하지 않는다. ## 22. Observability Spring의 `graphql.request`, `graphql.datafetcher`, `graphql.dataloader` observation을 재사용하고 플랫폼은 naming과 cardinality를 통제한다. 허용 tag: ```text operationName operationType clientProfile persisted outcome errorCategory complexityBucket depthBucket bounded schemaCoordinate bounded loaderName ``` 금지: ```text raw query variables userId raw tenantId objectId cursor token connection_init payload arbitrary full field path ``` Trace: ```text HTTP/WebSocket receive → GraphQL request → resolver → DataLoader → Application Use Case → DB/HTTP/Messaging ``` ## 23. Configuration과 startup validation ```yaml backend: graphql: execution-profile: BLOCKING_MVC http-profile: V1 schema: mapping-inspection: FAIL compatibility-policy: stable-v1 security: production-named-operation-required: true graphiql-enabled: false clients: first-party: max-document-bytes: 65536 max-variables-bytes: 65536 max-depth: 12 max-fields: 500 max-aliases: 50 max-fragments: 50 default-page-size: 20 max-page-size: 100 max-complexity: 10000 max-response-nodes: 10000 max-response-bytes: 5242880 max-execution-time: 5s ``` 위 숫자는 예시이며 benchmark 후 profile manifest에서 확정한다. Startup 실패 조건: - schema mapping mismatch - duplicate scalar/directive - forbidden `Upload` scalar - Stable에서 HTTP GET, batch, multipart 활성화 - Production anonymous operation 허용 - Production GraphiQL 활성화 - unknown client policy - resolver execution type과 runtime profile 충돌 - unsigned cursor codec - response cache 활성화 - Stable Starter에 Advanced module 자동 포함 ## 24. 테스트 전략 ### 24.1 Contract - SDL parse, assembly, mapping, compatibility - HTTP media, request error 4xx, execution error 200, partial data - immutable request context와 actor/tenant isolation - resolver return/input type와 direct repository access 금지 - DataLoader batch, missing, per-key error, cache scope - signed cursor와 forward/backward pagination - mutation transaction, optimistic conflict, idempotency - error masking과 null propagation - parser, alias, fragment, complexity, response budget - observation cardinality ### 24.2 Storage·Integration - JPA query count, entity load, fetch profile - Mongo examined documents, query count, keyset - HTTP batch downstream와 bulkhead - Fileserver upload ticket only ### 24.3 Performance·Fault - named high concurrency - deep valid query - wide alias/fragment bomb - nested connection - DataLoader saturation - DB pool saturation - downstream timeout - large response serialization - virtual thread saturation - event-loop blocking - cancellation leak ### 24.4 Realtime Advanced - connection-init auth - token expiry - slow consumer - source failure - ordered/low-latency - cancel storm - rolling deployment - graceful drain - 1k baseline과 목표 connection soak ## 25. 지원 등급 | 기능 | 등급 | |---|---| | SDL, Query/Mutation, HTTP POST | Stable | | Partial data/error | Stable | | Context·Security | Stable | | DataLoader | Stable | | signed Cursor Connection | Stable | | Cost Control | Stable | | Preparsed Cache | Stable | | MVC VT / WebFlux profile | Stable | | Fetch Profile | Stable platform capability | | Persisted Operation | Advanced Stable | | WebSocket Subscription | Advanced Stable | | SSE | Advanced | | Federation Subgraph | Advanced | | Codegen | Optional | | DataLoader chaining | Advanced | | RSocket·HTTP GET | Experimental | | Incremental Delivery | Experimental/disabled | | Multipart Upload·HTTP batch | Unsupported | | Entity/Document auto exposure | Unsupported default | | request-wide DB transaction | Unsupported default | | Response Cache | Initial unsupported | ## 26. 구현 단계 ```text Foundation → Schema / HTTP / Context / Resolver / Error / Security Execution Safety → DataLoader / Pagination / Cost / Timeout / Observability Storage Integration → Fetch Profile / JPA / Mongo / Downstream Batch Governance → Persisted Operation / Usage / Admin Realtime → WebSocket / SSE / Backpressure / Auth Lifecycle Extension → Federation / Codegen / RSocket / HTTP Draft / Incremental ``` ## 27. Definition of Done Stable 플랫폼은 다음 조건을 모두 만족한다. - Entity·Document가 GraphQL wire type으로 노출되지 않는다. - Schema mapping mismatch가 startup/CI에서 실패한다. - Schema breaking change가 Release Gate에서 차단된다. - HTTP 4xx/200 partial-error 계약이 고정된다. - N+1 회귀 테스트가 실제 PostgreSQL·MongoDB에서 통과한다. - Cursor 변조, filter/profile mismatch가 거부된다. - Query cost와 response size가 실행 전·중 제한된다. - internal exception, raw query, variables, PII가 응답·metric에 노출되지 않는다. - timeout 이후 하위 작업이 취소되고 resource가 누수되지 않는다. - Stable Starter가 Advanced 기능을 자동 활성화하지 않는다. - 실제 부하·장애 증거와 운영 Runbook이 존재한다. ## 28. 명시적 비지원 ```text GraphQL multipart upload HTTP array batching Arbitrary JSON input gateway Persistence entity auto exposure GraphQL request-wide transaction Durable subscription guarantee Exactly-once subscription delivery Raw GraphQL engine access for application code Unbounded list and totalCount-by-default Response cache without actor/tenant/permission model ``` --- # 부록 A. 요구사항 추적표 | 리서치 영역 | 설계 반영 | |---|---| | 조사 결론과 지원 기준 | §2~§6 | | Schema 계약과 진화 정책 | §7~§8 | | Transport와 실행·데이터 접근 계약 | §9~§12 | | DataLoader·Pagination·Mutation·Error | §13~§16 | | 보안·비용 통제·Persisted Operation | §17~§19 | | Subscription·Federation·Codegen·관측성 | §20~§22 | | 테스트·지원 등급·구현 순서 | §24~§27 | # 부록 B. 입력 심층 리서치 원문 > 아래 원문은 설계 결정의 근거와 세부 제약을 보존하기 위해 첨부한다. 상단 설계 계약이 구현 기준이다. # GraphQL API 실행 플랫폼 심층 리서치 ## 조사 결론과 지원 기준 이번 `graphql` 모듈의 적절한 정체성은 **GraphQL Java/Spring for GraphQL의 편의 Wrapper가 아니라, 외부 Schema 계약이 실제 Application Use Case로 실행되는 전 과정을 통제하는 API 실행 플랫폼**입니다. GraphQL Java는 `GraphQLSchema`, `DataFetcher`, 실행 전략과 `ExecutionResult`를 제공하는 실행 엔진이고, Spring for GraphQL은 이를 Spring의 transport, annotated controller, context propagation, exception resolution, DataLoader, Spring Data 통합과 연결합니다. Spring for GraphQL의 현재 Data Integration 문서 역시 GraphQL을 Selection Set을 SQL이나 JSON query로 일대일 번역하는 데이터 게이트웨이로 보지 않으며, client selection과 server-side projection을 상호 보완적인 것으로 설명합니다. citeturn16search4turn24view0 따라서 핵심 경계는 다음으로 확정하는 것이 가장 안전합니다. ```text Client ↓ GraphQL Transport ↓ Authentication + GraphQlRequestContext ↓ Parse / Validation ↓ Operation Policy / Cost / Persisted Operation ↓ Resolver / DataFetcher ↓ DataLoader ↓ Application Use Case ↓ JPA / MongoDB / HTTP Client / Messaging / Fileserver ↓ Read Model / DTO ↓ GraphQL Completion + Partial Data + Error ↓ HTTP Response / Subscription Stream ``` ```text GraphQL Platform owns ├─ SDL contract ├─ transport profile ├─ execution policy ├─ request context ├─ resolver conventions ├─ DataLoader contract ├─ cursor envelope ├─ error wire contract ├─ cost / abuse control ├─ persisted operation ├─ subscription delivery contract ├─ schema governance ├─ observability └─ test / release gates Domain/Application owns ├─ use cases ├─ authorization decision requiring domain state ├─ DTO / read models ├─ transaction requirements ├─ pagination query semantics ├─ filter / sort semantics └─ integration-event meaning Persistence/Integration modules own ├─ JPA fetch/query/transaction ├─ Mongo consistency/aggregation/index ├─ HTTP timeout/retry/TLS ├─ Messaging ACK/replay/DLQ ├─ Fileserver binary lifecycle └─ Object Storage bytes ``` ### 기술 기준선 2026년 8월 12일 기준 공식 문서에서 Spring Boot의 현재 stable은 `4.1.0`, Spring for GraphQL의 최신 stable은 `2.0.4`, GraphQL Java 공식 v25 문서는 `25.0`을 사용 버전으로 제시합니다. 완성된 GraphQL 언어·실행 규격은 **September 2025 Edition**입니다. 반면 GraphQL over HTTP는 현재도 **Stage 2 Draft**이며, 문서 자체가 production에서 draft를 고정 규격처럼 의존하는 것을 경고합니다. 따라서 HTTP 표준은 “지원 Profile”로 고정하고 사양 변화에 대한 별도 compatibility lane을 운영해야 합니다. citeturn19search4turn14search3turn16search12turn15search0turn22view0 | 영역 | 권장 기준 | 플랫폼 판정 | |---|---|---| | Java | 21 | Stable baseline | | Spring Boot | 프로젝트 4.1 BOM | Source of Truth | | Spring for GraphQL | 2.0 계열, 현재 2.0.4 | Stable | | GraphQL Java | Boot 조합 우선, 현재 v25 계열 | Stable | | GraphQL Specification | September 2025 | Contract | | GraphQL over HTTP | Stage 2 Draft | Compatibility Profile | | HTTP | Spring MVC / WebFlux | Stable | | WebSocket | Spring GraphQL + `graphql-ws` protocol | Advanced Stable | | SSE | Spring `GraphQlSseHandler` | Advanced | | RSocket | Spring 전용 Extension | Experimental | | Federation Subgraph | `federation-jvm` 통합 | Advanced | | Federation Router | Core 밖 | Experimental/별도 프로젝트 | | Multipart Upload | 지원하지 않음 | Unsupported | | HTTP multi-operation batch | 초기 지원하지 않음 | Unsupported | | Reactive | Reactor | Stable profile | | Virtual Thread | Java 21 + Spring executor | Stable profile | **중요한 최신 조사 결과가 하나 있습니다.** 현재 GraphQL over HTTP Draft의 상태 코드 부분에는 partial `data + errors`에 `294`를 제안하는 새 문구가 들어가 있는 반면, Spring for GraphQL 2.0.4는 GraphQL request가 validation을 통과한 뒤 발생한 execution/field error를 HTTP `200`으로 반환한다고 명시합니다. 같은 Draft의 field-error 설명에도 실행된 operation의 field error는 `200`이라는 규칙이 남아 있어, 이 영역은 현재 이동 중인 표준입니다. 따라서 **Stable 플랫폼에서 294를 선제 도입하지 말고 Spring 2.0.4의 실제 동작을 계약으로 고정**한 뒤, HTTP Draft 호환 Job에서 변화만 추적하는 것이 맞습니다. citeturn16search10turn23view1 ### 공개 기능 계층 | 계층 | 공개 대상 | 범위 | 정책 | |---|---|---|---| | **G1 Standard GraphQL API** | 일반 도메인 개발자 | SDL, Query/Mutation, annotated resolver, HTTP, validation, error, DataLoader, cursor pagination | 기본 Starter | | **G2 Advanced Execution** | 복잡한 API 개발자 | persisted operation, fetch profile, custom directive/scalar, advanced cost, subscription, SSE | 명시 opt-in | | **G3 GraphQL Extension** | 전문 통합 코드 | Federation, RSocket, provider codegen, custom WebSocket extension | 별도 모듈 | | **G4 Admin Plane** | 운영·CI/CD | schema diff, persisted-op registry, operation block, cost profile, usage, composition | 애플리케이션 resolver와 분리 | 일반 애플리케이션에는 `graphql.GraphQL`이나 자유형 `GraphQLCodeRegistry`를 기본 API로 공개하지 않는 편이 좋습니다. Spring Boot 자체도 일반적인 애플리케이션은 직접 `DataFetcher`를 작성하기보다 annotated controller를 사용하도록 안내하고, `RuntimeWiringConfigurer`는 scalar, directive, type resolver 같은 infrastructure extension을 위한 진입점으로 제공합니다. citeturn19search1turn18search2 권장 의존 구조는 다음과 같습니다. ```text graphql-core-api ↑ graphql-schema ↑ graphql-execution ↑ graphql-controller ↑ graphql-spring-boot-starter graphql-http ─────────┐ graphql-websocket ────┤ graphql-sse ──────────┤→ graphql-execution graphql-dataloader ───┤ graphql-pagination ───┤ graphql-security ─────┤ graphql-cost-control ─┤ graphql-error ────────┤ graphql-observability ┘ Optional: graphql-persisted-operation graphql-codegen graphql-federation graphql-spring-data-compat graphql-testkit-* ``` 특히 `graphql-spring-data-compat`를 별도 모듈로 두는 근거가 분명합니다. Spring for GraphQL 2.0.4는 `@GraphQlRepository`로 Querydsl/QBE repository를 자동 DataFetcher 등록할 수 있고, 자동 pagination은 **offset 기반, 기본 20개**입니다. 이 기능은 편리하지만 Persistence Model과 외부 Schema의 결합 및 pagination 정책 고착 가능성이 있으므로 Skeleton의 주류 API로 삼기보다 allowlist 기반 호환 기능으로 한정하는 것이 적절합니다. citeturn24view0 ## Schema 계약과 진화 정책 GraphQL Java는 programmatic schema와 SDL을 모두 제공하지만, 어느 방식을 선택해야 할지 확신이 없다면 SDL을 권장합니다. Spring Boot는 기본적으로 `src/main/resources/graphql/**`의 `.graphqls`, `.gqls`를 읽고, `classpath*:graphql/**/` 형태로 여러 모듈의 schema fragment도 조립할 수 있습니다. 따라서 **SDL First + module-owned fragment + platform-governed assembly**를 기본 계약으로 확정하는 것이 적절합니다. citeturn17search5turn19search1 ```text modules/order/src/main/resources/graphql/order/ ├── order-type.graphqls ├── order-query.graphqls └── order-mutation.graphqls modules/graphql-schema/src/main/resources/graphql/common/ ├── scalar.graphqls ├── error.graphqls ├── connection.graphqls └── directive.graphqls ``` 플랫폼은 Schema 자체를 독점하지 않습니다. **도메인 모듈이 자기 Schema coordinate와 resolver를 소유하고**, `graphql-schema`는 이름 규칙, scalar, directive, schema assembly, validation, compatibility checker를 소유해야 합니다. ### Type System 계약 **Output과 Input은 분리**합니다. ```graphql type Order { id: ID! status: OrderStatus! createdAt: Instant! } input CreateOrderInput { customerId: ID! items: [CreateOrderItemInput!]! } ``` JPA Entity, Mongo Document 또는 provider SDK model을 Input/Output으로 재사용하지 않습니다. 이는 Persistence refactoring을 API breaking change로 만드는 것을 방지하고, 입력에 공개해서는 안 될 persistence field가 포함되는 것을 차단합니다. **Nullability는 단순 Java null annotation이 아니라 장애 격리 경계**로 취급해야 합니다. GraphQL에서 nullable이 기본이고 `!`가 Non-Null이며, Non-Null field가 실행 중 null이 되면 오류가 부모 Non-Null 경계를 따라 전파됩니다. 따라서 `String!`은 “평상시 DB에서 null이 아니다”라는 의미가 아니라 **정상 데이터, 권한 처리, dependency failure, resolver mapping을 포함해 이 field를 항상 제공할 수 있다**는 API 보장이어야 합니다. citeturn15search0 권장 규칙은 다음과 같습니다. | 상황 | 권장 | |---|---| | Aggregate identity | `ID!` | | 반드시 존재하는 immutable value | Non-Null 후보 | | 외부 서비스가 채우는 enrichment | Nullable 우선 | | 권한에 따라 값을 반환하지 못할 수 있음 | Nullable 또는 별도 권한 모델 | | Resolver가 부분 실패할 수 있는 expensive child | Nullable boundary | | List 자체가 항상 존재 | `[T!]!` 후보 | | List element 자체가 실패 가능 | `[T]` 또는 구조 재설계 | | 신규 field | 기본 nullable로 도입 후 보장 검증 | | Non-Null 전환 | 데이터 + resolver contract gate 필수 | GraphQL `ID`는 외부 API 관점에서 opaque identifier로 사용해야 하며, 데이터베이스 PK 형식을 GraphQL contract로 약속하지 않는 것이 좋습니다. DB가 `Long`, `UUID`, Mongo `ObjectId`여도 resolver mapper에서 external ID로 변환하면 됩니다. GraphQL 규격 자체도 ID를 식별자용 scalar로 정의하며 string 형태로 직렬화합니다. citeturn15search0 **September 2025 규격의 `@oneOf`**는 정확히 하나의 input field가 제공되고 그 값이 non-null이어야 하는 Input Object를 표현합니다. 각 구성 field 자체는 nullable이고 default value를 가질 수 없습니다. 따라서 상호 배타적인 여러 selector를 임의 validator보다 Schema 자체로 표현하는 데 적합합니다. citeturn15search0 ```graphql input OrderSelector @oneOf { id: ID orderNumber: String externalReference: String } ``` 플랫폼에서는 `@oneOf`를 **Spec-Stable 기능**으로 분류하되 GraphQL Java/Spring 조합의 schema build 및 coercion contract test를 release gate에 포함하는 것이 좋습니다. **Scalar 정책**은 다음처럼 제한하는 것이 적절합니다. | Scalar | 판정 | 계약 | |---|---|---| | `ID` | Stable | opaque | | `UUID` | Stable custom | canonical string | | `Instant` | Stable custom | UTC timestamp | | `Date` | Stable custom | calendar date | | `BigDecimal` | Stable custom | 정확도 손실 없는 문자열/명시 coercion | | `Long` | Stable custom | JS client 범위 고려 | | `URL` | Advanced | parser/normalization 명시 | | `Email` | Advanced | format validation과 ownership validation 분리 | | `JSON` | Restricted | 명시 필드에만 allowlist | | `Upload` | Unsupported | Fileserver 사용 | 자유형 `JSON`은 typed GraphQL validation을 우회하므로 “모든 것을 넣는 escape hatch”로 제공해서는 안 됩니다. Custom scalar에는 명확한 serialization/coercion contract를 두고, 필요하면 GraphQL 규격의 `@specifiedBy`를 사용해 의미를 명시할 수 있습니다. citeturn15search0 Spring for GraphQL은 `graphql-multipart-request-spec`을 직접 지원하지 않으며 공식 문서도 GraphQL이 텍스트 데이터 교환을 중심으로 하고 별도의 비공식 multipart 규격이 존재한다고 설명합니다. 따라서 `Upload` scalar, multipart parser, binary streaming을 Core에 넣지 않고 기존 Fileserver에서 upload reservation/ticket을 발행하는 구조가 적합합니다. citeturn23view2 ```text GraphQL createFileUpload(...) ↓ Fileserver Application Use Case ↓ fileId + upload URL/ticket Binary Client ──────────→ Fileserver/Object Storage GraphQL ↓ metadata / status / reference only ``` ### Schema assembly과 startup 검증 Spring for GraphQL 2.0.4의 `SchemaMappingInspector`는 Schema field에 DataFetcher 또는 Java property mapping이 있는지, 존재하지 않는 Schema field에 DataFetcher가 등록됐는지, argument와 nullness가 Schema와 일치하는지 등을 startup에서 검사할 수 있습니다. 이를 단순 INFO report로 남기지 말고 Stable 플랫폼에서는 **CI 실패 또는 startup failure policy**로 승격하는 것이 좋습니다. citeturn18search3 Schema build gate는 최소한 다음 계약을 검사해야 합니다. ```text SDL parse → GraphQL schema validation → duplicate type / field / directive → scalar wiring → interface / union TypeResolver → resolver mapping inspection → argument mapping → nullability mapping → forbidden scalar/directive → schema compatibility → schema fingerprint ``` ### Schema Evolution과 breaking-change 기준 September 2025 규격에서는 field뿐 아니라 argument, input field, enum value에도 `@deprecated`를 적용할 수 있습니다. 다만 default가 없는 required non-null argument/input field는 바로 deprecate할 수 없으며, 먼저 nullable로 만들거나 default를 부여해야 합니다. citeturn15search0 플랫폼 compatibility checker는 단순 “SDL diff”와 **wire compatibility**, **generated-client source compatibility**를 구분해야 합니다. | 변경 | Wire 판정 | Generated Client 위험 | 기본 정책 | |---|---|---|---| | nullable output field 추가 | 호환 | 낮음 | 허용 | | non-null output field 추가 | 기존 operation에는 호환 | 생성 모델 변경 | 데이터 보장 검증 | | field 삭제/rename | Breaking | 높음 | 금지 | | output `T! → T` | Breaking | 높음 | 금지 | | output `T → T!` | 대체로 강화 | source type 변경 가능 | Review | | optional argument 추가 | 호환 | 낮음 | 허용 | | required argument 추가 | Breaking | 높음 | 금지 | | input `T → T!` | Breaking | 높음 | 금지 | | input `T! → T` | 호환 방향 | 생성 모델 변경 | 허용+Review | | optional input field 추가 | 호환 | 낮음 | 허용 | | input field 삭제 | Breaking | 높음 | 금지 | | enum value 삭제 | Breaking | 높음 | 금지 | | output enum value 추가 | protocol additive | exhaustive switch 위험 | Client impact review | | union/interface possible type 추가 | protocol additive | exhaustive codegen 위험 | Client impact review | | scalar coercion 변경 | 사실상 Breaking | 높음 | 새 Scalar/version | | directive 의미 변경 | Behavioral Breaking 가능 | 다양 | Review | | deprecated element 유지 | 호환 | 경고 | Usage gate 적용 | 삭제 정책은 다음과 같이 운영하는 편이 좋습니다. ```text @deprecated(reason: "Use ...") ↓ Schema Usage Observation ↓ Persisted Operation Reference Scan ↓ 지원 종료 기간 ↓ Client owner 승인 ↓ Breaking Schema Release ``` Schema hash 하나만으로는 충분하지 않습니다. `GraphQlSchemaContract`에는 최소 `schemaHash`, `breakingPolicyVersion`, `scalarManifestVersion`, `directiveManifestVersion`을 포함시키는 것이 좋습니다. ## Transport와 실행·데이터 접근 계약 Spring for GraphQL의 핵심 실행 추상화는 `ExecutionGraphQlService`이고, HTTP·WebSocket 등 transport가 여기에 요청을 위임합니다. 이 구조를 플랫폼의 실제 내부 경계로 그대로 활용하면 transport policy와 GraphQL execution policy를 분리하기 좋습니다. citeturn18search0 권장 파이프라인은 다음입니다. ```text Transport Adapter ↓ Request Envelope Validation ↓ Authentication ↓ GraphQlRequestContext creation ↓ WebGraphQlInterceptor ↓ Persisted Operation lookup ↓ Parse / Validate ↓ OperationPolicy ↓ CostPolicy ↓ ExecutionGraphQlService ↓ Annotated Controller / DataFetcher ↓ Application Service ↓ Completion / Error Mapping ``` ### Transport 지원 매트릭스 GraphQL over HTTP Draft는 POST를 MUST로 하고 GET을 MAY로 정의하지만, 현재 Spring for GraphQL의 server HTTP profile은 JSON body를 사용하는 POST를 기본 계약으로 합니다. Spring은 `application/graphql-response+json`에서 parse/validation failure에 4xx를 사용하고, validation을 통과해 execution이 시작된 뒤의 오류는 GraphQL `errors`와 HTTP 200으로 반환합니다. citeturn22view0turn23view1 | Transport | Spring 기능 | 등급 | 플랫폼 계약 | |---|---|---|---| | HTTP POST JSON | 기본 지원 | **Stable** | Query/Mutation | | HTTP GET Query | HTTP Draft는 허용, Spring 기본 server profile과 차이 | Experimental | 초기 비지원 | | `application/graphql-response+json` | 지원 | **Stable Preferred** | 새 client 기본 | | legacy `application/json` response | 지원 | Stable Compat | 이전 client | | WebSocket | `graphql-ws` 기반 | **Advanced Stable** | Subscription 중심 | | SSE | Distinct Connection | Advanced | Subscription-only | | RSocket | request-response/request-stream | Experimental | 내부 시스템 한정 | | multipart upload | Spring 직접 미지원 | Unsupported | Fileserver | | HTTP array batch | GraphQL core가 아님 | Unsupported | 필요 시 별도 Extension | Spring의 SSE 구현은 POST `application/json` + `Accept: text/event-stream`을 사용하고 **Distinct connections mode**만 구현하며, Query/Mutation이 아니라 Subscription의 대안으로 문서화되어 있습니다. WebSocket은 현재 `graphql-ws` 계열 protocol을 사용하고 과거 `subscriptions-transport-ws`는 inactive/superseded 상태입니다. RSocket에서는 Query/Mutation이 request-response, Subscription이 request-stream으로 처리됩니다. citeturn23view1turn23view3 HTTP Stable Profile은 다음처럼 명시하는 것이 좋습니다. ```text Method: POST only Content-Type: application/json Accept: application/graphql-response+json preferred application/json compatibility Body: query operationName variables extensions Policies: requestBytes variableBytes extensions allowlist named-operation requirement in production CORS allowlist CSRF profile according to credential mode compression threshold responseBytes request timeout ``` GraphQL over HTTP Draft도 `query`, `operationName`, `variables`, `extensions`라는 요청 parameter를 정의하고, request media type은 `application/json`, response media type은 `application/graphql-response+json`으로 규정하고 있습니다. 다만 Stage 2 Draft이므로 `GraphQlHttpProfile.V1`처럼 플랫폼 Profile을 명시적으로 versioning하는 것이 중요합니다. citeturn22view0 ### 실행 Profile Spring for GraphQL은 기본적으로 GraphQL Java의 비동기 실행 모델을 활용하며, reactive resolver는 `CompletionStage` 형태로 execution에 결합되고 Subscription에서는 `Publisher`가 유지됩니다. Java 21에서는 `@SchemaMapping`, `@BatchMapping` 등의 blocking `Callable`을 Virtual Thread executor에 보낼 수 있고, Spring Boot는 `spring.threads.virtual.enabled` 설정 시 annotated controller용 virtual-thread executor를 구성합니다. citeturn18search0turn18search2 권장 Profile은 세 가지입니다. | Profile | 적합한 workload | 규칙 | |---|---|---| | `BLOCKING_MVC` | JPA, blocking Mongo, blocking SDK | Java 21 VT 또는 bounded executor | | `REACTIVE_WEBFLUX` | Reactive Mongo, WebClient, 높은 Subscription 수 | blocking 금지, Reactor Context | | `MIXED_CONTROLLED` | 기존 blocking + 일부 reactive | 명시 adapter 필수, event-loop blocking 검출 | `MIXED`를 “아무 반환 타입이나 허용”이라는 의미로 사용해서는 안 됩니다. Resolver catalog에 `BLOCKING`, `ASYNC`, `REACTIVE`, `STREAM` 실행 유형을 등록하고 platform test가 WebFlux event loop에서 blocking repository가 호출되지 않는지 검증하는 편이 안전합니다. Spring은 GraphQL 전체 요청에 `TimeoutWebGraphQlInterceptor`를 제공하며 timeout 시 reactive data fetcher 쪽으로 cancellation 신호를 전달합니다. Streaming request에서는 stream이 성립될 때까지만 이 request timeout이 적용되고, 장기 Subscription에는 transport별 timeout을 별도로 구성해야 합니다. citeturn18search0turn18search4 따라서 timeout은 한 값이 아니라 다음 계층으로 나누어야 합니다. ```text transportHandshakeTimeout requestExecutionTimeout resolverBudget databaseDeadline httpClientDeadline dataLoaderBatchTimeout subscriptionIdleTimeout subscriptionMaxAge shutdownDrainTimeout ``` ### Resolver와 Application Service 경계 Annotated controller는 Spring for GraphQL에서 Schema field와 DataFetcher를 연결하는 표준적 고수준 진입점입니다. `@QueryMapping`, `@MutationMapping`, `@SubscriptionMapping`, `@SchemaMapping`, `@BatchMapping`을 주류 API로 사용하고, raw `DataFetcher` 등록은 custom scalar/directive/federation 등 명시된 SPI로 제한하는 것이 적절합니다. citeturn18search2 ```java @Controller final class OrderGraphQlController { private final FindOrderUseCase findOrder; private final CreateOrderUseCase createOrder; @QueryMapping OrderView order(@Argument String id, GraphQlRequestContext context) { return findOrder.find(new FindOrderQuery(context.actor(), id)); } @MutationMapping CreateOrderPayload createOrder( @Argument CreateOrderInput input, GraphQlRequestContext context) { return createOrder.execute(input.toCommand(context.actor())); } } ``` ```text Allowed in Resolver ├─ GraphQL input coercion 이후 transport DTO 변환 ├─ Bean Validation ├─ Actor/Tenant/Locale context 전달 ├─ Application Use Case 호출 └─ GraphQL DTO/Payload mapping Not Allowed ├─ EntityManager query ├─ MongoTemplate query ├─ HTTP retry/circuit breaker 구현 ├─ Kafka/Rabbit ACK 처리 ├─ ObjectStorage binary IO ├─ multi-step domain state transition └─ provider exception을 그대로 client에 노출 ``` 반환 기본형은 `DTO`, `ReadModel`, `Connection`, `MutationPayload`, `Publisher`이고 `Entity`, `Document`, provider SDK model, 자유형 `Map`은 금지 후보입니다. ### Selection Set과 Fetch Profile Spring for GraphQL의 공식 Data Integration 문서는 Selection Set을 DB query로 직접 번역하는 gateway가 아니라고 명시합니다. 또한 DTO/interface projection과 Selection Set을 함께 사용할 수 있다고 설명합니다. citeturn24view0 따라서 다음 방식이 가장 안전합니다. ```text GraphQL Selection ↓ SelectionClassifier ↓ registered FetchProfile ↓ Application Query ↓ JPA/Mongo Repository ``` 예를 들면: ```text Order.BASIC id status createdAt Order.WITH_ITEMS BASIC + items Order.WITH_CUSTOMER BASIC + customer Order.FULL_DETAIL BASIC + items + customer + paymentSummary ``` GraphQL 플랫폼은 `DataFetchingFieldSelectionSet`을 보고 **유한 집합의 Fetch Profile 중 하나를 선택**할 수 있지만, 실제 SQL join fetch, EntityGraph, DTO projection, Mongo aggregation은 JPA/Mongo 모듈에 남겨둡니다. 이 구조는 다음 실패를 동시에 막습니다. ```text Selection 조합마다 SQL plan 생성 필드 추가가 즉시 DB column exposure로 연결 computed field를 DB column으로 오인 권한 field를 projection에 잘못 포함 association lazy access로 N+1 발생 Mongo/JPA의 서로 다른 fetch semantics를 GraphQL이 흡수 ``` `@GraphQlRepository` 자동 노출은 이 원칙을 우회할 수 있으므로 `graphql-spring-data-compat`에서 등록 가능한 repository와 argument/filter를 명시적으로 allowlist해야 합니다. Spring의 자동 등록 기능 자체가 GraphQL arguments를 Querydsl predicate나 QBE로 바꾸고 offset pagination까지 수행하기 때문에, 플랫폼의 기본 추상화로 사용하면 저장소 세부가 Schema 계약 쪽으로 빠르게 올라옵니다. citeturn24view0 ## DataLoader·Pagination·Mutation·Error 계약 ### DataLoader는 요청 단위 Batch Planner GraphQL Java/Spring의 DataLoader는 Graph 탐색 중 발생하는 반복 fetch를 모아 batch load하고, 요청 범위 cache를 이용해 동일 key의 중복 load를 줄이는 수단입니다. Spring for GraphQL은 `BatchLoaderRegistry`와 `@BatchMapping`을 제공하고, DataLoader cache는 요청 내부에서 동작합니다. citeturn14search5turn18search2 따라서 다음 규칙을 Stable contract로 삼는 것이 좋습니다. ```text DataLoader instance → 반드시 request scoped DataLoader cache → 한 GraphQL execution 내부만 Cross-request caching → Redis / Application Cache responsibility Authorization → BatchLoader도 동일 Actor/Tenant Context 사용 Batch I/O → JPA IN / Mongo $in / HTTP batch API 한도에 맞춰 chunk Result → ordered loader면 key와 동일 순서 → mapped loader면 key 기반 명시 대응 ``` 요청 간 DataLoader singleton을 공유하지 않는 것이 특히 중요합니다. 사용자·tenant별 데이터가 cache에 남는 경우 교차 사용자 데이터 유출까지 이어질 수 있기 때문입니다. GraphQL Java의 DataLoader 문서도 사용자별 데이터를 다루는 경우 per-request DataLoader 사용을 권장합니다. citeturn10search3 Batch 계약은 다음 데이터를 보존해야 합니다. ```java record GraphQlBatchPolicy( String loaderName, int maxBatchSize, Duration timeout, MissingKeyPolicy missingKeyPolicy, BatchErrorPolicy errorPolicy ) {} ``` `@BatchMapping`에서 ordered collection을 반환할 경우 source/parent와 같은 순서여야 하고, `Map` 반환형을 이용하면 key별 대응을 명시할 수 있습니다. citeturn18search2 GraphQL Java 25에는 chained DataLoader의 자동 dispatch 기능이 추가됐지만 opt-in이고 dispatch ordering을 변화시킬 수 있으므로 **G2 Advanced + 별도 회귀 테스트**가 적절합니다. 기존 N+1 해결만을 위해 Stable 기본값으로 켜지 않는 것이 좋습니다. citeturn10search3 DataLoader와 저장소 기술의 선택 기준은 다음과 같습니다. | 문제 | 우선 수단 | |---|---| | 동일 parent type의 child ID 반복 조회 | DataLoader | | Root query 자체가 과도한 Entity graph 적재 | DTO Projection / Fetch Profile | | JPA 단일 aggregate에서 반드시 함께 읽음 | JPA fetch plan | | Mongo 내 `$lookup`이 본질적으로 적합 | Mongo Aggregation | | Downstream이 batch API 제공 | HTTP batch DataLoader | | Downstream이 batch API 없음 | application aggregator + concurrency/bulkhead | | unbounded child collection | DataLoader가 아니라 Pagination | ### Cursor Connection Spring for GraphQL 2.0.4는 Connection/Edge/PageInfo 패턴과 `first`, `after`, `last`, `before` 입력을 지원하며 Spring Data `Window`와 `Slice`를 Connection으로 adapter할 수 있습니다. 또한 keyset cursor를 JSON으로 직렬화하고 Base64 encoding하는 전략도 제공합니다. citeturn24view0 다만 **Base64는 encoding이지 무결성 보호가 아닙니다.** Backend Skeleton에서 cursor를 security-sensitive server state token으로 취급한다면 다음 envelope를 권장합니다. ```json { "v": 1, "profile": "orders-by-created-at", "direction": "FORWARD", "keyset": { "createdAt": "2026-08-12T01:00:00Z", "id": "..." }, "filter": "sha256:...", "kid": "cursor-key-2026-01", "mac": "..." } ``` Cursor 계약: ```text Opaque to client Versioned Query profile bound Filter fingerprint bound Sort/tie-breaker included HMAC authenticated Unknown version rejected Page size revalidated server-side Sensitive raw values 최소화 ``` 저장소별 실제 pagination은 GraphQL이 아닌 storage/application layer가 소유합니다. ```text JPA → keyset / ScrollPosition MongoDB → range predicate + _id tie-breaker External API → upstream opaque cursor를 signed envelope 안에 보관 ``` `totalCount`는 기본 Connection field로 강제하지 않는 편이 좋습니다. 큰 relation에서는 page fetch보다 count가 더 비쌀 수 있기 때문에, 필요한 Connection profile에만 explicit resolver로 제공합니다. ### Mutation과 Transaction GraphQL 규격은 Mutation root field를 문서 순서대로 serial execution하지만, **그 순차성이 데이터베이스 transaction을 의미하지는 않습니다**. Spring for GraphQL 2.0.4의 현재 Data Integration 문서도 GraphQL 자체에 transaction semantics가 없다고 명시하고, transaction-per-controller-method를 가장 단순한 권장 방식으로 설명합니다. 여러 DataFetcher 전체에 request-wide transaction을 걸려면 execution을 serial하게 만드는 등 훨씬 큰 제약이 필요합니다. citeturn15search0turn24view0 따라서 플랫폼 기본 계약은 다음으로 확정하는 것이 좋습니다. ```text Mutation Resolver ↓ one Application Use Case ↓ one explicit transaction boundary ``` 더 구체적으로는 transaction annotation 자체도 Resolver보다 Application Service에 두는 것이 아키텍처 경계를 더 잘 유지합니다. ```java @Service final class UpdateOrderService implements UpdateOrderUseCase { @Transactional public UpdateOrderResult execute(UpdateOrderCommand command) { // domain operation } } ``` ```text mutation { updateOrder(...) createInvoice(...) } ``` 위 두 root field는 **순차 실행될 뿐 독립 Use Case Transaction**입니다. 두 작업이 반드시 atomic해야 한다면 client가 root mutation 두 개를 조합하도록 두지 말고: ```graphql type Mutation { confirmOrderAndCreateInvoice( input: ConfirmOrderInput! ): ConfirmOrderPayload! } ``` 처럼 **하나의 Application Use Case를 표현하는 Mutation**을 제공하는 것이 맞습니다. Spring 문서 역시 여러 변경을 하나의 transaction으로 유지해야 한다면 필요한 모든 input을 하나의 mutation method가 받도록 설계하는 방식을 권고합니다. citeturn24view0 Optimistic concurrency도 GraphQL 자체 기능이 아니라 Use Case contract로 전달합니다. ```graphql input UpdateOrderInput { orderId: ID! expectedVersion: Long! status: OrderStatus! } ``` Idempotency 역시 플랫폼 Extension입니다. 권장 범위는 HTTP request 전체가 아니라 **side-effecting Mutation Use Case**입니다. ```text idempotencyKey + actor/client identity + mutation coordinate + normalized business input hash → Idempotency Record ``` HTTP `Idempotency-Key`를 수용하더라도 이를 Mutation Context로 변환해 Application Use Case의 idempotency mechanism에 전달해야 하며, GraphQL transport 자체가 DB replay policy를 구현해서는 안 됩니다. ### Error contract GraphQL의 중요한 장점은 실행 중 field error가 발생해도 가능한 data를 함께 반환할 수 있다는 점입니다. Non-Null field error는 부모 경계로 전파될 수 있으므로 Error model과 Nullability policy는 함께 설계해야 합니다. citeturn15search0 권장 네 계층은 다음과 같습니다. | 계층 | 발생 시점 | `path` | HTTP Stable Profile | 표현 | |---|---|---:|---|---| | `REQUEST_ERROR` | parse/validation/coercion | 보통 없음 | 4xx with new media type | `errors` | | `FIELD_ERROR` | resolver execution | 있음 | 200 | partial `data` + `errors` | | `BUSINESS_RESULT` | 예상 가능한 업무 결과 | 보통 data | 200 | typed payload/union 우선 | | `INTERNAL_ERROR` | 예상 밖 장애 | 있음/없음 | 실행 후면 200 | opaque `errors` | Spring for GraphQL에서 unresolved DataFetcher exception은 기본적으로 `INTERNAL_ERROR`와 `executionId`가 들어간 의도적으로 불투명한 message로 바뀌며, request execution 전에 발생한 global parse/validation error는 `DataFetcherExceptionResolver`가 처리할 수 없습니다. Subscription publisher의 사후 오류에는 별도 `SubscriptionExceptionResolver`가 있습니다. citeturn18search0turn23view0 권장 Wire Error는 다음과 같습니다. ```json { "message": "요청을 처리할 수 없습니다.", "path": ["order", "payment"], "extensions": { "code": "PAYMENT_DEPENDENCY_UNAVAILABLE", "category": "DEPENDENCY", "retryable": true, "executionId": "..." } } ``` 공개 가능한 `extensions`는 allowlist로 고정해야 합니다. ```text code category retryable executionId constraint field // safe logical input field only ``` 다음은 공개 금지입니다. ```text Java exception class stack trace SQL JPQL Mongo query HTTP downstream URL with credentials provider error body database identifier tenant id raw value access token internal host ``` 예상 가능한 업무 상태는 가능하면 GraphQL errors보다 typed data로 표현하는 방식을 병행할 수 있습니다. ```graphql union CreateOrderResult = CreateOrderSuccess | OrderAlreadyExists | InvalidOrderState ``` 단, 모든 validation을 union으로 바꾸는 것도 바람직하지 않습니다. **Schema/input coercion 문제는 request error, 예상 가능한 업무 분기는 typed result, 예상 밖 실행 실패는 GraphQL error**라는 기준이 가장 일관됩니다. ## 보안·비용 통제·Persisted Operation GraphQL endpoint는 URL 하나를 공유하므로 HTTP URL security만으로 operation이나 field별 접근권한을 구분할 수 없습니다. Spring for GraphQL도 이 점을 명시하며 서비스나 data-fetching 계층에서 `@PreAuthorize`, `@Secured` 같은 fine-grained security를 적용하도록 안내합니다. citeturn18search1 보안 모델은 다음 계층으로 분리해야 합니다. ```text Transport Authentication ↓ Client Profile Authorization ↓ Operation Authorization ↓ Field / Use Case Authorization ↓ Object Authorization ↓ Tenant Isolation ``` **Schema visibility와 authorization은 별개**입니다. GraphQL Java의 `GraphqlFieldVisibility`는 Schema에서 특정 field를 보이지 않게 할 수 있지만, object instance에 대해 사용자가 실제로 접근해도 되는지를 검증하는 수단은 아닙니다. 따라서 “introspection에서 숨겼다 = 보호됐다”라는 설계를 금지해야 합니다. citeturn17search18turn18search1 권장 request context는 transport DTO와 domain context를 뒤섞지 않고 immutable하게 구성합니다. ```java record GraphQlRequestContext( Actor actor, TenantContext tenant, ClientProfile clientProfile, Locale locale, String operationId, String traceId, Deadline deadline ) {} ``` `tenantId`를 GraphQL argument에서 받아 신뢰하지 않고 authentication/session으로 결정된 `TenantContext`를 Application Service와 BatchLoader까지 전달해야 합니다. ### Introspection·GraphiQL Spring Boot에서 Schema introspection은 기본 허용이며 설정으로 비활성화할 수 있고, GraphiQL은 기본 비활성입니다. citeturn19search1 권장 운영 정책은 다음과 같습니다. | 환경 | Introspection | GraphiQL | Schema Printer | |---|---|---|---| | Local | Allow | Allow | Allow | | Test | Allow | Optional | CI only | | Dev | Authenticated | Authenticated | Admin | | Staging | Admin/CI profile | Off | Admin | | Prod internal | Client profile 기반 | Off | G4 Admin | | Prod public | 제한 또는 Off | Off | Off | Introspection을 꺼도 resolver authorization이나 cost defense가 대체되지 않습니다. 실제 방어는 Field/Object Authorization, Operation Policy, Cost Limit, request size limit, Persisted Operation 등에서 이루어져야 합니다. ### Operation cost와 DoS 방어 GraphQL Java 25에는 parser 수준의 query character/token/whitespace/grammar-depth 제한과 query depth/complexity instrumentation이 있습니다. 공식 문서의 library 기본 ceiling은 query characters 약 1 MiB, token 15,000, whitespace token 200,000, grammar rule depth 500 등으로 상당히 넓으므로, 이를 그대로 public API의 business limit로 사용하는 대신 플랫폼에서 더 작은 profile을 별도로 두는 것이 좋습니다. citeturn10search1turn10search2 방어는 “Depth 한 개”가 아니라 다음 순서가 적절합니다. ```text HTTP body bytes ↓ variables bytes ↓ persisted-operation lookup / request format ↓ parser character/token/grammar limits ↓ GraphQL parse ↓ GraphQL validation ↓ operation count/name/type ↓ introspection policy ↓ selection depth ↓ field/alias/fragment count ↓ input list/string limits ↓ complexity ↓ estimated response node budget ↓ execution timeout ↓ runtime response-byte limit ``` Complexity는 단순 field count보다 **cardinality와 resolver 특성**을 포함해야 합니다. ```text FieldCost = baseFieldCost + resolverWeight + childCost × effectiveCardinality ``` 예를 들면 다음처럼 catalog를 관리할 수 있습니다. | Resolver class | 상대 weight 예 | |---|---:| | in-memory scalar/property | 1 | | indexed DB lookup | 2 | | batched relation | 3 | | bounded aggregation | 8 | | external service batch | 10 | | external per-object request | 20 | | search/heavy aggregation | 별도 승인 | 위 숫자는 표준값이 아니라 **초기 calibration용 상대 weight**입니다. Release benchmark에서 실제 latency, DB statement 수, examined-row/document 수, downstream call 수와 비교해 조정해야 합니다. 특히 List/Connection은 다음처럼 계산해야 합니다. ```text requested first = 50 child subtree cost = 10 connection cost ≈ root cost + 50 × child cost ``` Client가 `first`를 생략했다고 비용을 1로 계산해서는 안 되고 `defaultPageSize`를 사용해야 하며, `first > maxPageSize`는 execution 전에 거부해야 합니다. Cost Profile은 다음처럼 구성할 수 있습니다. ```java record GraphQlClientPolicy( int maxDocumentBytes, int maxVariablesBytes, int maxDepth, int maxFields, int maxAliases, int maxFragments, int maxInputListElements, int maxPageSize, long maxComplexity, long maxResponseNodes, Duration maxExecutionTime, boolean introspectionAllowed, boolean persistedOperationOnly ) {} ``` ```text PUBLIC PARTNER FIRST_PARTY ADMIN INTROSPECTION ``` 각 실제 숫자는 부하 시험을 통과한 **환경별 manifest**에 두고 애플리케이션 코드에 산재시키지 않는 것이 좋습니다. ### Operation name GraphQL 규격상 단일 anonymous operation은 유효할 수 있지만, 운영 환경에서 anonymous operation은 tracing, cost exception, persisted registry, usage 분석을 어렵게 합니다. 따라서 다음 정책이 실용적입니다. ```text Local → anonymous 허용 Dev/Test → 경고 Production FIRST_PARTY/PARTNER → named operation 필수 PUBLIC → named operation 또는 persisted-only ``` ### Preparsed cache와 Persisted Operation 분리 GraphQL Java의 `PreparsedDocumentProvider`는 parsing/validation 결과인 Document를 재사용할 뿐 **실행 결과를 cache하지 않습니다**. 공식 문서도 이 점을 명확히 구분합니다. citeturn16search4 따라서 플랫폼의 세 cache를 절대 합치지 않아야 합니다. | 기능 | Cache 대상 | 권한 민감도 | 기본 | |---|---|---|---| | Preparsed Document Cache | parse/validation 결과 | schema/profile 의존 | Enabled bounded | | Persisted Operation Registry | 승인된 operation text | client/profile 의존 | Advanced Stable | | Response Cache | 실행 결과 | actor/tenant/permission 매우 민감 | Disabled | Preparsed key는 raw query 하나만으로 끝내기보다 적어도 다음을 고려해야 합니다. ```text documentHash schemaContractHash validationPolicyVersion clientSchemaProfile ``` Field visibility나 validation rule이 client profile별로 달라지는데 query 문자열만 cache key로 사용하면 잘못 검증된 Document를 재사용할 가능성이 있기 때문입니다. Persisted Operation registry는 다음 형태가 적절합니다. ```text PersistedOperation ├─ operationId ├─ operationName ├─ sha256Document ├─ canonicalDocument ├─ schemaContractHash ├─ allowedClientProfiles ├─ maximumComplexity ├─ maximumVariablesBytes ├─ status: ACTIVE | DEPRECATED | BLOCKED ├─ registeredAt └─ expiresAt? ``` 실행은 다음처럼 합니다. ```text operationId ↓ registry lookup ↓ client allowlist ↓ document hash / schema compatibility ↓ variable validation ↓ cost policy ↓ execute ``` 이렇게 하면 incident 시 특정 operation만 G4 Admin Plane에서 `BLOCKED` 처리할 수 있습니다. ### WebSocket 인증과 장기 권한 Spring for GraphQL에는 WebSocket `connection_init` payload에서 인증 정보를 꺼내 인증한 후 SecurityContext를 이후 request로 전파하는 interceptor가 있습니다. citeturn18search12 그러나 장기 Subscription에서 인증은 connection 시점 한 번으로 끝내면 안 됩니다. Stable 정책 후보는 **credential expiry 시 connection 종료**입니다. ```text connection_init → authenticate → capture actor / tenant / credential expiry → connection_ack → subscribe → initial authorization → events → token expiry or revocation signal → complete/close ``` resource ownership이 수시로 바뀌는 매우 민감한 Subscription이라면 각 event를 Integration Event에서 GraphQL DTO로 바꾸기 전에 Application Authorization Service에 재검증하도록 별도 Profile을 둡니다. ## Subscription·Federation·Codegen·관측성 GraphQL Specification은 Subscription을 source event stream으로부터 response stream을 만드는 장기 operation으로 정의하지만, **transport protocol, ACK, buffering, replay, resend, QoS는 정의하지 않습니다**. 따라서 GraphQL Subscription을 Kafka/RabbitMQ 등의 durable messaging으로 간주할 수 없습니다. citeturn6search0turn22view0 경계는 다음과 같습니다. ```text Messaging Platform ├─ persistence ├─ offset ├─ ACK ├─ replay ├─ retry └─ DLQ GraphQL Subscription ├─ client selection ├─ connected actor context ├─ authorization ├─ event → GraphQL DTO ├─ live delivery └─ cancellation ``` 권장 source 구조는 다음입니다. ```text Kafka / Rabbit / Application Publisher ↓ Subscription Source Adapter ↓ authorization / filtering ↓ Publisher ↓ GraphQL selection completion ↓ WebSocket or SSE ``` 외부 Integration Event를 그대로 GraphQL type으로 반환하지 말고 stable GraphQL DTO로 변환해야 합니다. Messaging schema와 GraphQL schema의 lifecycle이 달라야 하기 때문입니다. ### Subscription 상태 모델 ```text CONNECTING → AUTHENTICATING → READY → SUBSCRIBED → STREAMING → CANCELLING → COMPLETED Exceptional: AUTH_EXPIRED SLOW_CONSUMER SOURCE_FAILED SERVER_DRAINING PROTOCOL_ERROR ``` 관리할 connection policy: ```text connectionInitTimeout heartbeat/ping-pong idleTimeout maxConnectionAge maxSubscriptionsPerConnection maxBufferedEvents slowConsumerPolicy subscriptionAuthPolicy shutdownDrain sourceCancellation ``` Spring WebFlux WebSocket handler는 non-blocking I/O와 backpressure를 사용하며 Subscription은 Reactive Streams `Publisher`로 처리됩니다. 따라서 수천~수만 장기 connection을 주요 목표로 한다면 Subscription transport는 `REACTIVE_WEBFLUX`가 기본 release lane이 되는 것이 타당합니다. citeturn23view1 기본 slow-consumer 정책은 **무음 event drop보다 connection 종료**를 권장합니다. GraphQL 자체에는 replay 표준이 없기 때문에 drop하면 client가 어느 event를 잃었는지 알 수 없습니다. Event loss가 업무상 허용되는 telemetry 성격의 기능만 별도 `LOW_LATENCY_DROP_ALLOWED` profile을 사용할 수 있습니다. ### Event ordering Spring for GraphQL 2.0.4 문서는 nested asynchronous field fetch 때문에 Subscription item이 source 순서와 다르게 완료될 수 있음을 설명하며, `SubscriptionExecutionStrategy.KEEP_SUBSCRIPTION_EVENTS_ORDERED` flag로 buffering해 source ordering을 유지할 수 있습니다. citeturn23view0 따라서 두 Profile을 분리하는 것이 좋습니다. | Profile | 보장 | 대가 | |---|---|---| | `LOW_LATENCY` | 완료되는 즉시 전달 | event 순서 변화 가능 | | `ORDERED` | upstream ordering 보존 | head-of-line blocking/버퍼 증가 | ### Replay Extension GraphQL 표준 자체에는 resume token이 없으므로 다음 기능을 GraphQL Stable Core의 보장으로 광고하지 않습니다. citeturn6search0 내구성 있는 재연결이 실제 요구라면 G2/G3 Extension으로: ```text snapshotSequence eventSequence messagingOffset subscriptionCursor snapshot + live handoff ``` 를 정의하고, 실제 replay guarantee는 Messaging Platform이 소유하도록 해야 합니다. ### SSE와 WebSocket Spring SSE는 Distinct Connection mode이므로 Subscription 한 개당 HTTP streaming connection이라는 운영적 특성을 고려해야 합니다. HTTP/2가 connection 비용을 줄일 수 있지만 WebSocket multiplexing과 운영 특성이 다릅니다. citeturn23view1 권장 선택은: ```text 브라우저 양방향 프로토콜·여러 subscription multiplexing → WebSocket 단순 server→client stream, proxy 친화성 중요 → SSE Advanced Spring 내부 ecosystem의 RSocket 요구 → RSocket Experimental ``` ### Federation Spring for GraphQL 2.0.4는 `federation-jvm` 통합과 `FederationSchemaFactory`, `@EntityMapping`을 제공하고 federated entity batch loading도 지원합니다. citeturn17search1 그러나 **Federation 지원 가능 = Skeleton 기본 architecture여야 함**은 아닙니다. 권장 등급: ```text Single executable schema → Stable Default Federation Subgraph → Advanced Optional Federation Router/Supergraph 운영 → 별도 프로젝트 또는 Experimental Schema Stitching → Core 비지원 ``` Federation activation gate는 다음 조건을 요구하는 것이 좋습니다. ```text 독립 배포되는 서비스 + schema ownership이 실제로 팀별 분리 + composition CI + router 운영 owner + distributed trace + cross-subgraph latency budget + entity key lifecycle policy + partial-failure policy ``` 그렇지 않으면 Federation은 단순 Schema 분리보다 cross-subgraph N+1, network amplification, 배포 순서, entity key 변경, 권한 중복 같은 훨씬 큰 운영 비용을 가져옵니다. ### Code Generation Spring for GraphQL 2.0.4의 Code Generation 문서는 DGS Codegen을 통해 client request/input/response selection types와 Schema data type을 생성할 수 있다고 설명하면서, **애플리케이션 자체의 data type은 로직을 넣어야 할 경우 code generation이 이상적이지 않을 수 있고 client type은 좋은 후보**라고 명시합니다. citeturn17search0 따라서 정책은 다음처럼 명확합니다. ```text Schema validation → 적극 사용 Operation validation → 적극 사용 Client request/response model generation → 지원 Transport-only DTO generation → 선택 지원 Domain Entity generation → 금지 Application Use Case interface generation → 금지 Repository generation from GraphQL schema → 금지 ``` 즉: ```text SDL ↓ Generated GraphQL Transport Types (optional) ↓ Mapper ↓ Human-owned Application DTO / Use Case ``` 를 유지합니다. ### 관측성 Spring for GraphQL은 Micrometer 기반으로 `graphql.request`, non-trivial `graphql.datafetcher`, `graphql.dataloader` observation을 제공합니다. DataFetcher에는 field name/outcome/error type 등이, DataLoader에는 loader name/outcome/size 등이 관측 정보로 제공됩니다. citeturn14search2 플랫폼은 Spring observation을 재구현하지 말고 **명명 규칙과 cardinality policy를 추가**해야 합니다. ```text Request graphql.request ├─ operationName ├─ operationType ├─ clientProfile ├─ persisted ├─ outcome ├─ errorCategory ├─ complexityBucket ├─ depthBucket ├─ duration └─ responseBytes Resolver graphql.datafetcher ├─ schemaCoordinate ├─ resolverCatalog ├─ outcome └─ duration DataLoader graphql.dataloader ├─ loaderName ├─ batchSize ├─ requestedKeys ├─ cacheEffect └─ duration ``` 저 cardinality catalog에 등록된 `operationName`, `schemaCoordinate`, `loaderName`은 사용할 수 있지만 raw query와 argument를 metric label로 사용해서는 안 됩니다. ```text Metric/Trace에 금지 ├─ raw GraphQL document ├─ variables ├─ userId ├─ raw tenantId ├─ object ID ├─ cursor ├─ Authorization header ├─ connection_init token └─ arbitrary full field path ``` Trace에는 다음 구간이 연결되어야 합니다. ```text HTTP/WebSocket receive → GraphQL request → resolver → DataLoader → Application Use Case → DB/HTTP/Messaging ``` GraphQL Java v25의 Profiler는 DataFetcher, DataLoader와 execution timing 분석 기능을 제공하므로 Local/Dev 성능 분석이나 G4 Admin diagnostic에 유용하지만, public GraphQL response의 일반 extension으로 노출하지 않는 편이 적절합니다. citeturn17search15 ## 테스트·지원 등급·구현 순서와 Release Gate Spring의 `GraphQlTester`는 transport-independent 테스트 workflow를 제공하고 `HttpGraphQlTester`, `WebSocketGraphQlTester`, `RSocketGraphQlTester`, `ExecutionGraphQlServiceTester`, `WebGraphQlTester` 등의 변형을 제공합니다. 따라서 같은 operation document를 execution-level과 실제 transport-level에서 반복 검증하는 구조를 만들 수 있습니다. citeturn14search8 ### 지원 범위 최종안 | Capability | 등급 | Release 조건 | |---|---|---| | SDL-first schema | **Stable** | schema/build compatibility gate | | Query/Mutation annotated resolver | **Stable** | application boundary test | | HTTP POST | **Stable** | media/status contract | | `application/graphql-response+json` | **Stable preferred** | client compatibility | | Partial data/error | **Stable** | null propagation test | | Request context/security | **Stable** | actor/tenant isolation | | DataLoader/BatchMapping | **Stable** | N+1/batch isolation | | Cursor Connection | **Stable** | signed cursor/keyset tests | | Cost control | **Stable** | attack + load tests | | Preparsed cache | **Stable** | bounded/cache-key test | | Persisted Operation | **Advanced Stable** | registry/admin tooling | | Selection→registered Fetch Profile | **Advanced Stable** | plan/query regression | | Virtual-thread MVC | **Stable Profile** | blocking workload load test | | Reactive WebFlux | **Stable Profile** | BlockHound/equivalent gate | | WebSocket Subscription | **Advanced Stable** | soak/reconnect/auth/cancel | | SSE Subscription | **Advanced** | connection scalability | | DataLoader chaining v25 | **Advanced** | dispatch regression | | Federation Subgraph | **Advanced** | composition/trace/failure gate | | Client Codegen | **Optional Stable tooling** | generated-source compatibility | | Server transport DTO Codegen | Advanced | mapping policy | | RSocket | **Experimental** | explicit consumer | | HTTP GET | **Experimental** | HTTP draft compatibility | | Federation Router | **Experimental/외부** | independent ops | | Incremental delivery | **Experimental/초기 비지원** | spec/framework profile 확정 후 | | Response Cache | **초기 비지원** | actor/tenant/cache-key 모델 확정 전 | | Multipart Upload | **Unsupported** | Fileserver 사용 | | HTTP array batch | **Unsupported** | 별도 extension 없이는 금지 | | Entity/Document 자동 API 노출 | **Unsupported as default** | compat allowlist만 | | GraphQL request-wide DB TX | **Unsupported as default** | 특수 instrumentation만 | ### 계약 테스트 매트릭스 | 영역 | 반드시 검증할 계약 | |---|---| | Schema | SDL parse, duplicate, scalar, interface/union, mapping inspection, golden snapshot | | Compatibility | field/arg/input/nullability/enum/union/directive diff | | HTTP | media type, malformed JSON, parse/validation 4xx, execution error 200 | | Resolver | DTO return, direct repository ban, context propagation | | Query | variables, fragment, alias, directive, partial data | | DataLoader | query count, duplicate key, missing key, ordering, cache scope, tenant isolation | | Pagination | forward/backward, tie value, concurrent insert/delete, cursor HMAC, version | | Mutation | transaction, second root failure, optimistic conflict, idempotency | | Error | request/field/business/internal, null propagation, masking | | Security | unauthenticated, field/object/tenant, introspection, WS auth | | Cost | chars, token, depth, alias bomb, fragment bomb, nested list, variable size | | Persisted | hash mismatch, schema mismatch, client deny, blocked operation | | Subscription | connect, cancel, auth expiry, ordering, slow consumer, source failure | | Federation | composition, entity key, batch entity load, partial subgraph failure | | Observability | no raw query/PII tag, trace correlation, cardinality | | Shutdown | in-flight query, WS drain, cancellation propagation | ### 성능·장애 Release Gate Stable 선언 전에 다음 benchmark는 실제 storage/integration test 환경과 결합해야 합니다. ```text Named query high concurrency Deep-but-valid query Wide alias query Nested Connection DataLoader batch saturation JPA connection pool saturation Mongo pool saturation HTTP downstream bulkhead saturation Dependency timeout Partial dependency failure Large response serialization Virtual thread saturation Event-loop blocking detection ``` Subscription lane은 별도로: ```text 1k persistent connections → 10k target connection test → multiple subscriptions per connection → event burst → slow consumer → cancel storm → auth expiry → server restart → rolling deployment → graceful drain → source restart ``` Spring WebSocket의 reactive path가 backpressure를 제공한다고 하더라도 **GraphQL 플랫폼의 bounded buffer와 downstream event source가 자동으로 안전해지는 것은 아니므로**, 실제 slow consumer 시 memory profile과 cancellation propagation을 계측해야 합니다. citeturn23view1 ### 실무 실패 사례와 설계 규칙 | 실패 상황 | 직접 원인 | 플랫폼 규칙 | 회귀 테스트 | |---|---|---|---| | JPA Entity 직접 GraphQL 반환 | Persistence/API 결합, lazy access | DTO/ReadModel only | persistence association access 검사 | | Field마다 repository query | N+1 | BatchMapping/DataLoader | DB query count | | 전역 DataLoader | request 간 cache leakage | per-request loader | actor/tenant cross-request test | | unbounded child list | response 폭증 | Connection + maxPage | complexity/node budget | | depth만 제한 | wide alias attack | depth + fields + alias + complexity | alias bomb | | selection→SQL 직접 생성 | plan 조합 폭증 | finite FetchProfile | query-plan snapshot | | root mutations를 하나의 TX로 오인 | serial ≠ atomic | Use Case TX | second mutation failure | | 모든 GraphQL error를 HTTP 500 | partial-data semantics 훼손 | request/field error 분리 | partial response | | 내부 exception message 노출 | 정보 유출 | opaque INTERNAL_ERROR | SQL/stack leak | | 자유형 JSON 입력 | Schema validation 우회 | typed input | unexpected-field/injection | | `@GraphQlRepository` 무제한 사용 | persistence exposure | compat allowlist | forbidden filter/sort | | multipart upload | fileserver 기능 중복 | upload ticket pattern | Upload scalar absence | | Subscription을 durable queue로 간주 | replay/ACK 표준 없음 | Messaging + live adapter | disconnect loss | | WS init 시점만 auth | 장기 권한 회수 미반영 | expiry/revalidation | role revoke/token expiry | | raw query를 metric tag | cardinality/PII 폭증 | named operation/catalog | cardinality budget | | usage 없이 field 삭제 | client breaking | deprecation + usage gate | schema diff | | Federation 선제 도입 | distributed complexity | single schema default | composition opt-in gate | | Base64 cursor를 신뢰 | client 변조 가능 | HMAC/version/fingerprint | tamper test | | `totalCount` 항상 계산 | expensive count | explicit opt-in | count query regression | | page size를 complexity에 미반영 | cheap-score bypass | list cardinality multiplier | `first=max` cost test | | resolver timeout만 둠 | downstream work 지속 | cancellation/deadline propagation | timeout leak | | reactive resolver에서 blocking DB | event-loop starvation | execution profile 검사 | event-loop blocking test | | request-wide transaction | long TX·병렬성 상실 | mutation use-case TX | concurrency/lock test | | field visibility를 auth로 사용 | 실제 object 권한 누락 | service/object auth | hidden-but-direct access | | operation cache와 response cache 혼동 | stale/permission leak | cache 계층 분리 | cross-user test | | Subscription unordered completion | async child fetch | explicit ORDERED profile | sequence test | | slow client에서 무한 buffering | OOM | bounded buffer/close policy | slow consumer load | | Schema와 resolver mapping 불일치 | silent null | startup inspector | application startup failure | ### 단계별 구현 순서 **Foundation 단계**에서는 `graphql-core-api`, `graphql-schema`, `graphql-controller`, `graphql-http`, `graphql-error`, `graphql-security`, `graphql-testkit-core/http`를 구현합니다. 완료 조건은 SDL assembly, mapping inspection, HTTP media/status contract, immutable request context, DTO-only resolver convention, error masking, schema snapshot/compatibility test가 모두 CI에서 통과하는 것입니다. GraphQL Java의 SDL 권장 방식과 Spring의 schema resource/mapping inspection을 그대로 활용하고 재구현하지 않습니다. citeturn17search5turn18search3 ```text Foundation DONE = Schema Contract + HTTP POST + Context + Resolver Boundary + Error Contract + Security + Testkit ``` **Execution Safety 단계**에서는 `graphql-dataloader`, `graphql-pagination`, `graphql-cost-control`, `graphql-observability`를 추가합니다. 완료 조건은 N+1 회귀 테스트, signed keyset cursor, max-page enforcement, depth/alias/complexity attack test, request timeout/cancellation, operation-name 기반 low-cardinality observation이 통과하는 것입니다. Spring의 DataLoader, connection adapter, Micrometer observation을 활용하되 플랫폼은 policy와 manifest만 추가합니다. citeturn14search5turn24view0turn14search2 ```text Execution Safety DONE = No N+1 baseline regression + bounded pagination + cost budget + timeout/cancel + observability ``` **Operation Governance 단계**에서는 `graphql-persisted-operation`과 G4 Admin Plane을 구현합니다. 완료 조건은 immutable operation registry, schema hash 연동, client allowlist, operation block, usage measurement, preparsed cache와 registry 분리가 검증되는 것입니다. GraphQL Java의 `PreparsedDocumentProvider`는 parsed document cache일 뿐이라는 의미를 유지해야 합니다. citeturn16search4 ```text Governance DONE = Persisted Registry + Operation Block + Schema Usage + Deprecation Gate + Cost Profile Management ``` **Realtime 단계**에서 `graphql-websocket`, 이후 필요할 때 `graphql-sse`를 활성화합니다. 완료 조건은 connection-init authentication, auth expiry, max connection age, cancellation, slow consumer, bounded buffer, source error, rolling shutdown, ordered/low-latency profile, 1k→목표 connection soak test입니다. Spring은 WebSocket/SSE transport와 ordering hook을 이미 제공하므로 플랫폼이 wire protocol을 새로 만들 필요는 없습니다. citeturn23view0turn23view1turn18search12 ```text Realtime DONE = Auth lifecycle + bounded streaming + cancel + ordering profile + shutdown drain + load/soak test ``` **Extension 단계**에서만 `graphql-federation`, `graphql-codegen`, RSocket 등 G3 기능을 추가합니다. Federation Subgraph는 실제 독립 배포와 Schema ownership이 확인될 때만 Advanced로 승격하고, generated code는 GraphQL transport/client boundary에 한정합니다. Spring for GraphQL은 현재 Federation과 Codegen 양쪽 모두 공식 통합 지점을 제공하므로 Core에 별도 범용 framework를 만들 필요는 없습니다. citeturn17search1turn17search0 최종적으로 권장되는 플랫폼의 중심 API는 `GraphQL` 엔진 Wrapper가 아니라 다음 계약군입니다. ```text GraphQlSchemaContract GraphQlRequestContext GraphQlClientPolicy GraphQlOperationPolicy GraphQlCostPolicy GraphQlFetchProfile GraphQlBatchPolicy GraphQlCursorCodec GraphQlErrorContract PersistedOperationRegistry GraphQlSubscriptionPolicy GraphQlObservationConvention SchemaCompatibilityPolicy ``` 그리고 도메인 개발자에게 보이는 일상적인 코드는 가능한 한 평범하게 유지합니다. ```text SDL fragment + @QueryMapping / @MutationMapping / @SchemaMapping / @BatchMapping + Application Use Case + DTO / Read Model ``` 이것이 Spring for GraphQL과 GraphQL Java가 이미 잘하는 부분을 다시 추상화하지 않으면서도, **Schema 계약 → transport → 인증/context → parse/validation → operation/cost → resolver → DataLoader → application → partial result/error → streaming**이라는 전체 실행 경로에 일관된 안전 정책을 부여하는 가장 적절한 구조입니다. Spring의 현재 설계도 transport가 `ExecutionGraphQlService`를 호출하고, annotated controller가 DataFetcher로 연결되며, Spring Data 통합은 선택 사항으로 제공되는 계층 구조를 취하고 있습니다. citeturn18search0turn18search2turn24view0