Reactive GraphQL 마스터 노트 시리즈 7편 (마지막). DataLoader가 N+1을 자동 해결하는 메커니즘, Apollo Federation으로 마이크로서비스 GraphQL 통합, Schema Stitching·Subgraph·Gateway 패턴, Caching 전략, Persisted Queries, Observability, 운영 체크리스트까지 — 시리즈 마무리.
이 글은 Reactive GraphQL 마스터 노트 시리즈의 마지막 일곱 번째 편입니다. 1~6편이 토대였다면, 이번엔 고급·운영 — DataLoader·Federation·Caching·운영 모범 사례.
N+1 문제 깊은 해결. 마이크로서비스 GraphQL 통합 (Federation). Caching·Observability. 시리즈 마무리.
처음 고급 주제가 어렵게 느껴지는 이유
처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, DataLoader 동작 원리가 막연합니다. 둘째, Federation이 왜 필요한지 안 보입니다.
해결법은 한 가지예요. "DataLoader = 자동 batching + cache" + "Federation = 마이크로서비스 GraphQL 통합" 두 줄.
DataLoader — N+1 깊은 해결
N+1 문제 복습
{
users { # 10 사용자
posts { # 각 사용자 게시물 → 10 쿼리
title
}
}
}
11 쿼리 (1 + 10).
@BatchMapping (4편)
@BatchMapping(typeName = "User", field = "posts")
public Map<User, List<Post>> userPosts(List<User> users) { ... }
11 → 2 쿼리. 충분.
DataLoader — 더 강력
@Component
public class PostBatchLoader implements MappedBatchLoader<String, List<Post>> {
@Autowired
private PostRepository repo;
@Override
public CompletionStage<Map<String, List<Post>>> load(Set<String> userIds) {
return repo.findByAuthorIdIn(userIds)
.collectList()
.map(posts -> posts.stream()
.collect(Collectors.groupingBy(Post::getAuthorId)))
.toFuture();
}
}
@Configuration
public class GraphQLConfig {
@Bean
public DataLoaderRegistry dataLoaderRegistry(PostBatchLoader loader) {
DataLoaderRegistry registry = new DataLoaderRegistry();
registry.register("posts", DataLoader.newMappedDataLoader(loader));
return registry;
}
}
@SchemaMapping(typeName = "User", field = "posts")
public CompletableFuture<List<Post>> posts(User user, DataLoader<String, List<Post>> loader) {
return loader.load(user.getId());
}
여기서 정말 중요한 시험 함정 — DataLoader = batching + caching + deduplication:
- Batching — 같은 tick의 요청 묶음
- Caching — 같은 ID 재요청 시 캐시
- Deduplication — 중복 ID 자동 제거
@BatchMapping은 단순 batching만. DataLoader는 더 강력.
DataLoader vs BatchMapping
| 측면 | @BatchMapping | DataLoader |
|---|---|---|
| 단순함 | 단순 | 복잡 |
| 캐시 | X | 요청 단위 캐시 |
| Deduplication | 자동 | 자동 |
| 다중 사용 | 별도 메서드 | 같은 Loader 재사용 |
| 권장 | 일반 | 복잡·재사용 |
여기서 시험 함정이 하나 있어요. 일반 = @BatchMapping, 복잡 = DataLoader. 둘 다 N+1 해결.
GraphQL Federation — 마이크로서비스 통합
[User Service] (GraphQL)
[Post Service] (GraphQL)
[Comment Service] (GraphQL)
↓
[Apollo Gateway] (통합 Schema)
↓
[Client]
각 마이크로서비스 = 자기 GraphQL Subgraph. Gateway가 통합.
Subgraph
# user-service/schema.graphqls
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
}
# post-service/schema.graphqls
type Post @key(fields: "id") {
id: ID!
title: String!
authorId: ID!
}
# 다른 서비스 타입 확장
type User @key(fields: "id") {
id: ID! @external
posts: [Post!]! # 이 서비스가 추가
}
type Query {
post(id: ID!): Post
}
Gateway
# Apollo Router 또는 Apollo Gateway
subgraphs:
- name: users
routing_url: http://user-service/graphql
- name: posts
routing_url: http://post-service/graphql
# 클라이언트 쿼리 (통합)
{
user(id: "1") { # User Service
name
posts { # Post Service (자동 라우팅)
title
}
}
}
여기서 정말 중요한 시험 함정 — Federation = 모놀리식 GraphQL 회피. 각 팀이 자기 GraphQL 운영. Gateway가 통합. Apollo·DGS (Netflix) 도구.
Spring GraphQL Federation 지원
implementation 'com.apollographql.federation:federation-graphql-java-support:5.x'
@Bean
public RuntimeWiringConfigurer federation() {
return wiringBuilder ->
Federation.transform(...)
.resolveEntityType(...)
.build();
}
Caching 전략
Per-Request Cache
@QueryMapping
public Mono<User> user(@Argument String id, GraphQLContext ctx) {
Map<String, User> cache = ctx.get("userCache");
if (cache == null) {
cache = new HashMap<>();
ctx.put("userCache", cache);
}
if (cache.containsKey(id)) {
return Mono.just(cache.get(id));
}
return userRepo.findById(id)
.doOnSuccess(u -> cache.put(id, u));
}
요청 단위 캐시. DataLoader가 더 우아.
Application-Level Cache (Redis)
@QueryMapping
public Mono<User> user(@Argument String id) {
return cacheService.get("user:" + id, User.class)
.switchIfEmpty(
userRepo.findById(id)
.doOnSuccess(u -> cacheService.put("user:" + id, u, Duration.ofMinutes(5)))
);
}
Reactive Redis 시리즈 4편 패턴.
CDN Cache (Persisted Queries 시)
GET /graphql?queryId=abc&variables=...
↓ CDN cache (각 queryId별)
↓ 미스 시 서버
POST 쿼리는 CDN cache 어려움. GET + Persisted Queries = 가능.
Persisted Queries
1. 클라이언트 빌드 시 모든 쿼리를 ID로 등록
2. 런타임에 ID + 변수만 전송
3. 서버는 등록된 ID에서 쿼리 lookup
# 클라이언트
POST /graphql
{
"queryId": "GetUser_abc123",
"variables": { "id": "1" }
}
# 서버
{ user(id: "1") { name } } // 등록된 쿼리
여기서 정말 중요한 시험 함정 — Persisted Queries = 보안 + 성능:
- 등록 안 된 쿼리 거부 (보안)
- GET + 캐시 가능 (성능)
- 페이로드 크기 ↓
Apollo·Relay 자동 지원.
Observability
Metrics
management:
endpoints:
web:
exposure:
include: prometheus,metrics
자동 메트릭:
spring.graphql.request(지연·횟수)- 쿼리별 분리
Logging
@Bean
public RuntimeWiringConfigurer loggingInstrumentation() {
return wiringBuilder -> wiringBuilder.instrumentation(
new SimpleInstrumentation() {
@Override
public InstrumentationContext<ExecutionResult> beginExecution(...) {
log.info("Query: {}", parameters.getQuery());
return super.beginExecution(parameters, state);
}
}
);
}
쿼리 로그 + 지연.
Tracing — Apollo Tracing·OpenTelemetry
@Bean
public Instrumentation tracingInstrumentation(OpenTelemetry otel) {
return GraphQLTracingInstrumentation.builder(otel).build();
}
분산 추적·각 Resolver 시간.
Schema Versioning
# Deprecated 표시
type User {
id: ID!
name: String!
username: String! @deprecated(reason: "Use 'name' instead")
}
GraphQL = 추가는 호환·삭제는 deprecation.
여기서 시험 함정이 하나 있어요. REST의 v1·v2 같은 versioning 거의 X. 단일 스키마·필드 추가/deprecate. 사용 메트릭 보고 deprecation 안전.
Schema Stitching (옛 방식)
Federation 이전. 여러 GraphQL 서버 → 단일 Gateway. 수동 통합.
여기서 시험 함정이 하나 있어요. Federation이 신규 표준. Schema Stitching는 deprecated. Apollo 권장 = Federation.
Apollo Studio·Apollo Sandbox
- Schema 시각화
- 사용 메트릭
- 운영 로그·쿼리
- 팀 협업
GraphQL 운영 SaaS. 무료 티어 있음.
Subscription 운영
수만 동시 구독 시:
- WebSocket 연결 = OS 한계
- Pub/Sub 백엔드 (Redis·Kafka)
- Sticky Session 또는 Pub/Sub
- 모니터링 — 활성 구독 수
3편 (Subscription) 참조.
운영 체크리스트
✓ N+1 해결 — @BatchMapping 또는 DataLoader
✓ Depth Limit (10~15) + Complexity Limit
✓ Rate Limit (클라이언트별)
✓ Persisted Queries (운영)
✓ Introspection OFF (운영)
✓ HTTPS (TLS)
✓ JWT·OAuth2 인증
✓ @PreAuthorize 메서드 보안
✓ 필드 레벨 보안 (민감 정보)
✓ 캐싱 — Redis·CDN
✓ Federation (마이크로서비스)
✓ Apollo Studio 또는 자체 모니터링
✓ Subscription Pub/Sub 백엔드
✓ Schema deprecation 점진 전환
✓ 부하 테스트 (Apollo 도구·k6 등)
✓ 부분 응답 + 에러 처리
✓ Logging·Tracing (OpenTelemetry)
REST·gRPC와 함께 사용
[Client]
↓ GraphQL (외부)
[BFF (Backend for Frontend)]
↓ gRPC (내부 마이크로서비스)
[User Service·Order Service·...]
여기서 정말 중요한 시험 함정 — GraphQL = BFF 위치 적합. 외부 API = 클라이언트 친화. 내부 = gRPC·REST. 각자 자리.
시리즈 마무리 — 7편 종합
1편부터 7편까지의 흐름:
| 편 | 주제 | 한 줄 |
|---|---|---|
| 1 | 기본 개념 | Schema·SDL·REST 비교 |
| 2 | Query·Mutation | Variables·Alias·Fragment |
| 3 | Subscription | WebSocket·Sinks·Pub/Sub |
| 4 | Spring | 5 어노테이션·@BatchMapping |
| 5 | Reactive | Mono/Flux·R2DBC |
| 6 | Security·Testing | JWT·@GraphQlTest·Complexity |
| 7 | 고급 | DataLoader·Federation·운영 |
GraphQL의 거의 모든 운영 패턴을 한 번에 통찰할 토대.
시험 직전 한 번 더 — 자주 헷갈리는 함정 모음
여기까지가 7편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.
- DataLoader = batching + caching + deduplication
- @BatchMapping = 단순 batching만
- 일반 = @BatchMapping, 복잡 = DataLoader
- GraphQL Federation = 마이크로서비스 GraphQL 통합
- Subgraph + Gateway (Apollo·DGS·Apollo Router)
@key(fields: "id")+@external다른 서비스 타입 확장- Spring GraphQL Federation 지원 라이브러리
- Caching — Per-Request·Application (Redis)·CDN (Persisted)
- Persisted Queries = 등록된 ID만 + GET 캐시 가능
- 보안 + 성능 + 페이로드 ↓
- Observability — Spring metrics·Logging Instrumentation·OpenTelemetry
- Apollo Studio·Apollo Sandbox = 운영 SaaS
- Schema Versioning = deprecation (REST v1/v2 X)
- Schema Stitching → Federation (신규 표준)
- Subscription 운영 — Pub/Sub·Sticky Session·모니터링
- GraphQL = BFF 위치 적합 (외부 API)
- 내부 = gRPC·REST
- 운영 체크 — N+1·Depth·Rate·Persisted·Introspection·TLS·인증·캐시·Federation·모니터링·deprecation·부하 테스트
시리즈 다른 편 (시리즈 마지막)
- 1편 — 기본 개념·Schema
- 2편 — Query·Mutation·Variables
- 3편 — Subscription·실시간 구독
- 4편 — Spring for GraphQL
- 5편 — Reactive GraphQL
- 6편 — Security·Testing
- 7편 — 고급 (DataLoader·Federation·운영) (현재 글, 시리즈 마지막)
공식 문서: DataLoader / Apollo Federation / Spring GraphQL Reference 에서 더 깊이.
Reactive GraphQL 마스터 시리즈는 여기서 마무리. 1편부터 7편까지의 흐름이 머리에 남으면 GraphQL의 거의 모든 운영 패턴을 손에 잡고 시작할 토대가 됩니다.