Files
clean-architecture-backend-…/docs/superpowers/specs/2026-08-12-graphql-api-execution-platform-design.md

91 KiB
Raw Permalink Blame History

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으로 실행되는 전 과정을 통제하는 플랫폼의 설계 기준을 확정한다.

구현자가 다시 결정하지 않도록 다음 항목을 명시적으로 고정한다.

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. 핵심 결론

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 계열의 실제 동작을 계약으로 삼는다.

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. 공개 기능 계층

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. 확정 모듈 구조

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

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. 중심 공개 계약

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) {}
public interface GraphQlFetchProfileRegistry {
    GraphQlFetchProfile select(
        GraphQlSchemaCoordinate coordinate,
        Set<String> 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를 소유하고 플랫폼이 조립·검증한다.

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<String,Object>를 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에서 다음 순서를 실행한다.

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 승인 확인
@deprecated
→ Schema Usage Observation
→ Persisted Operation Reference Scan
→ Support Window
→ Client Owner Approval
→ Removal

9. HTTP Transport Profile

9.1 Stable profile

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 전에 제한
  • variablesextensions는 object만 허용
  • extensions key allowlist
  • Production named operation 필수
  • Cookie 인증 profile은 CSRF 필수
  • HTTP GET, array batch, multipart upload는 Stable 비지원

9.3 파일 Upload

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

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은 분리한다.

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으로 변환하지 않는다.

Selection Set
→ SelectionClassifier
→ registered finite FetchProfile
→ Application Query
→ JPA/Mongo implementation

예:

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

{
  "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

Mutation root field serial execution
≠ request-wide DB transaction

하나의 Mutation Resolver는 하나의 Application Use Case를 호출한다. 여러 변경이 원자적이어야 하면 하나의 명시적 Use Case Mutation을 제공한다.

Optimistic version과 idempotency key는 Application command로 전달한다.

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

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:

code
category
retryable
executionId
constraint
safe logical field

공개 금지:

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

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

방어 순서:

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를 포함한다.

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 경계

DataLoader Cache
→ request internal data loading

Preparsed Document Cache
→ parse/validation result

Persisted Operation Registry
→ approved operation document

Response Cache
→ Stable 초기 비지원

Preparsed cache key:

documentHash
schemaContractHash
validationPolicyVersion
clientSchemaProfile

Persisted Operation은 Advanced 모듈에서 다음을 소유한다.

operationId
operationName
sha256Document
canonicalDocument
schemaContractHash
allowedClientProfiles
maximumComplexity
maximumVariablesBytes
status: ACTIVE | DEPRECATED | BLOCKED

Incident 시 특정 operation을 application redeploy 없이 차단할 수 있어야 한다.

20. Subscription

Subscription은 live response stream이며 durable messaging이 아니다.

Messaging
→ persistence / ACK / replay / retry / DLQ

GraphQL Subscription
→ client selection / connected actor / authorization / live delivery / cancellation

상태:

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_LATENCYORDERED 분리
  • 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

허용:

client request/response
operation validation
transport-only DTO

금지:

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:

operationName
operationType
clientProfile
persisted
outcome
errorCategory
complexityBucket
depthBucket
bounded schemaCoordinate
bounded loaderName

금지:

raw query
variables
userId
raw tenantId
objectId
cursor
token
connection_init payload
arbitrary full field path

Trace:

HTTP/WebSocket receive
→ GraphQL request
→ resolver
→ DataLoader
→ Application Use Case
→ DB/HTTP/Messaging

23. Configuration과 startup validation

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. 구현 단계

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. 명시적 비지원

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

따라서 핵심 경계는 다음으로 확정하는 것이 가장 안전합니다.

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
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 + errors294를 제안하는 새 문구가 들어가 있는 반면, 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

권장 의존 구조는 다음과 같습니다.

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

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은 분리합니다.

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

input OrderSelector @oneOf {
  id: ID
  orderNumber: String
  externalReference: String
}

플랫폼에서는 @oneOfSpec-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

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는 최소한 다음 계약을 검사해야 합니다.

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 적용

삭제 정책은 다음과 같이 운영하는 편이 좋습니다.

@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

권장 파이프라인은 다음입니다.

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은 다음처럼 명시하는 것이 좋습니다.

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은 한 값이 아니라 다음 계층으로 나누어야 합니다.

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

@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()));
    }
}
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<T>, MutationPayload, Publisher<DTO>이고 Entity, Document, provider SDK model, 자유형 Map<String,Object>은 금지 후보입니다.

Selection Set과 Fetch Profile

Spring for GraphQL의 공식 Data Integration 문서는 Selection Set을 DB query로 직접 번역하는 gateway가 아니라고 명시합니다. 또한 DTO/interface projection과 Selection Set을 함께 사용할 수 있다고 설명합니다. citeturn24view0

따라서 다음 방식이 가장 안전합니다.

GraphQL Selection
      ↓
SelectionClassifier
      ↓
registered FetchProfile
      ↓
Application Query
      ↓
JPA/Mongo Repository

예를 들면:

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 모듈에 남겨둡니다.

이 구조는 다음 실패를 동시에 막습니다.

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로 삼는 것이 좋습니다.

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 계약은 다음 데이터를 보존해야 합니다.

record GraphQlBatchPolicy(
    String loaderName,
    int maxBatchSize,
    Duration timeout,
    MissingKeyPolicy missingKeyPolicy,
    BatchErrorPolicy errorPolicy
) {}

@BatchMapping에서 ordered collection을 반환할 경우 source/parent와 같은 순서여야 하고, Map<K,V> 반환형을 이용하면 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 WindowSlice를 Connection으로 adapter할 수 있습니다. 또한 keyset cursor를 JSON으로 직렬화하고 Base64 encoding하는 전략도 제공합니다. citeturn24view0

다만 Base64는 encoding이지 무결성 보호가 아닙니다. Backend Skeleton에서 cursor를 security-sensitive server state token으로 취급한다면 다음 envelope를 권장합니다.

{
  "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 계약:

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가 소유합니다.

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

따라서 플랫폼 기본 계약은 다음으로 확정하는 것이 좋습니다.

Mutation Resolver
      ↓
one Application Use Case
      ↓
one explicit transaction boundary

더 구체적으로는 transaction annotation 자체도 Resolver보다 Application Service에 두는 것이 아키텍처 경계를 더 잘 유지합니다.

@Service
final class UpdateOrderService implements UpdateOrderUseCase {

    @Transactional
    public UpdateOrderResult execute(UpdateOrderCommand command) {
        // domain operation
    }
}
mutation {
  updateOrder(...)
  createInvoice(...)
}

위 두 root field는 순차 실행될 뿐 독립 Use Case Transaction입니다. 두 작업이 반드시 atomic해야 한다면 client가 root mutation 두 개를 조합하도록 두지 말고:

type Mutation {
  confirmOrderAndCreateInvoice(
    input: ConfirmOrderInput!
  ): ConfirmOrderPayload!
}

처럼 하나의 Application Use Case를 표현하는 Mutation을 제공하는 것이 맞습니다. Spring 문서 역시 여러 변경을 하나의 transaction으로 유지해야 한다면 필요한 모든 input을 하나의 mutation method가 받도록 설계하는 방식을 권고합니다. citeturn24view0

Optimistic concurrency도 GraphQL 자체 기능이 아니라 Use Case contract로 전달합니다.

input UpdateOrderInput {
  orderId: ID!
  expectedVersion: Long!
  status: OrderStatus!
}

Idempotency 역시 플랫폼 Extension입니다. 권장 범위는 HTTP request 전체가 아니라 side-effecting Mutation Use Case입니다.

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_ERRORexecutionId가 들어간 의도적으로 불투명한 message로 바뀌며, request execution 전에 발생한 global parse/validation error는 DataFetcherExceptionResolver가 처리할 수 없습니다. Subscription publisher의 사후 오류에는 별도 SubscriptionExceptionResolver가 있습니다. citeturn18search0turn23view0

권장 Wire Error는 다음과 같습니다.

{
  "message": "요청을 처리할 수 없습니다.",
  "path": ["order", "payment"],
  "extensions": {
    "code": "PAYMENT_DEPENDENCY_UNAVAILABLE",
    "category": "DEPENDENCY",
    "retryable": true,
    "executionId": "..."
  }
}

공개 가능한 extensions는 allowlist로 고정해야 합니다.

code
category
retryable
executionId
constraint
field   // safe logical input field only

다음은 공개 금지입니다.

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로 표현하는 방식을 병행할 수 있습니다.

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

보안 모델은 다음 계층으로 분리해야 합니다.

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하게 구성합니다.

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 한 개”가 아니라 다음 순서가 적절합니다.

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 특성을 포함해야 합니다.

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은 다음처럼 계산해야 합니다.

requested first = 50
child subtree cost = 10

connection cost
≈ root cost + 50 × child cost

Client가 first를 생략했다고 비용을 1로 계산해서는 안 되고 defaultPageSize를 사용해야 하며, first > maxPageSize는 execution 전에 거부해야 합니다.

Cost Profile은 다음처럼 구성할 수 있습니다.

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
) {}
PUBLIC
PARTNER
FIRST_PARTY
ADMIN
INTROSPECTION

각 실제 숫자는 부하 시험을 통과한 환경별 manifest에 두고 애플리케이션 코드에 산재시키지 않는 것이 좋습니다.

Operation name

GraphQL 규격상 단일 anonymous operation은 유효할 수 있지만, 운영 환경에서 anonymous operation은 tracing, cost exception, persisted registry, usage 분석을 어렵게 합니다. 따라서 다음 정책이 실용적입니다.

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 하나만으로 끝내기보다 적어도 다음을 고려해야 합니다.

documentHash
schemaContractHash
validationPolicyVersion
clientSchemaProfile

Field visibility나 validation rule이 client profile별로 달라지는데 query 문자열만 cache key로 사용하면 잘못 검증된 Document를 재사용할 가능성이 있기 때문입니다.

Persisted Operation registry는 다음 형태가 적절합니다.

PersistedOperation
├─ operationId
├─ operationName
├─ sha256Document
├─ canonicalDocument
├─ schemaContractHash
├─ allowedClientProfiles
├─ maximumComplexity
├─ maximumVariablesBytes
├─ status: ACTIVE | DEPRECATED | BLOCKED
├─ registeredAt
└─ expiresAt?

실행은 다음처럼 합니다.

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 종료입니다.

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

경계는 다음과 같습니다.

Messaging Platform
├─ persistence
├─ offset
├─ ACK
├─ replay
├─ retry
└─ DLQ

GraphQL Subscription
├─ client selection
├─ connected actor context
├─ authorization
├─ event → GraphQL DTO
├─ live delivery
└─ cancellation

권장 source 구조는 다음입니다.

Kafka / Rabbit / Application Publisher
            ↓
    Subscription Source Adapter
            ↓
    authorization / filtering
            ↓
       Publisher<Event>
            ↓
GraphQL selection completion
            ↓
WebSocket or SSE

외부 Integration Event를 그대로 GraphQL type으로 반환하지 말고 stable GraphQL DTO로 변환해야 합니다. Messaging schema와 GraphQL schema의 lifecycle이 달라야 하기 때문입니다.

Subscription 상태 모델

CONNECTING
→ AUTHENTICATING
→ READY
→ SUBSCRIBED
→ STREAMING
→ CANCELLING
→ COMPLETED

Exceptional:
AUTH_EXPIRED
SLOW_CONSUMER
SOURCE_FAILED
SERVER_DRAINING
PROTOCOL_ERROR

관리할 connection policy:

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으로:

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

권장 선택은:

브라우저 양방향 프로토콜·여러 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여야 함은 아닙니다.

권장 등급:

Single executable schema
→ Stable Default

Federation Subgraph
→ Advanced Optional

Federation Router/Supergraph 운영
→ 별도 프로젝트 또는 Experimental

Schema Stitching
→ Core 비지원

Federation activation gate는 다음 조건을 요구하는 것이 좋습니다.

독립 배포되는 서비스
+ 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

따라서 정책은 다음처럼 명확합니다.

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
→ 금지

즉:

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를 추가해야 합니다.

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로 사용해서는 안 됩니다.

Metric/Trace에 금지
├─ raw GraphQL document
├─ variables
├─ userId
├─ raw tenantId
├─ object ID
├─ cursor
├─ Authorization header
├─ connection_init token
└─ arbitrary full field path

Trace에는 다음 구간이 연결되어야 합니다.

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 환경과 결합해야 합니다.

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은 별도로:

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

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

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

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

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가 아니라 다음 계약군입니다.

GraphQlSchemaContract
GraphQlRequestContext
GraphQlClientPolicy
GraphQlOperationPolicy
GraphQlCostPolicy
GraphQlFetchProfile
GraphQlBatchPolicy
GraphQlCursorCodec
GraphQlErrorContract
PersistedOperationRegistry
GraphQlSubscriptionPolicy
GraphQlObservationConvention
SchemaCompatibilityPolicy

그리고 도메인 개발자에게 보이는 일상적인 코드는 가능한 한 평범하게 유지합니다.

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