- Published on
GraphQL Federation 완전 가이드 2025: Apollo Federation, Supergraph, 분산 GraphQL
- Authors

- Name
- Youngju Kim
- @fjvbn20031
TL;DR
- Federation: 여러 GraphQL 서비스를 단일 supergraph로 통합. 마이크로서비스 환경의 표준
- Schema Stitching의 후계자: Federation v2가 stitching을 대체. 더 강력하고 명확
- Subgraph 설계: 도메인 단위로 분할. Entity로 subgraph 간 데이터 공유
- Apollo Router: Rust로 작성. Gateway보다 10배 빠름. CNCF 표준
- Netflix, Airbnb, Expedia, GitHub 모두 Federation 채택
1. 왜 GraphQL Federation이 필요한가?
1.1 모놀리식 GraphQL의 한계
[클라이언트]
↓
[GraphQL Server]
↓
[User Service] [Order Service] [Product Service] [...]
문제점:
- 단일 팀 병목: GraphQL 스키마가 한 곳에서 관리됨
- 거대한 코드베이스: 모든 도메인이 한 서버
- 배포 결합: 한 도메인 변경 = 전체 재배포
- 확장성: 단일 장애점
1.2 Schema Stitching (이전 시도)
[Stitching Gateway]
├─ User Schema
├─ Order Schema
└─ Product Schema
여러 GraphQL 스키마를 합쳐 하나로 제공.
한계:
- 수동 충돌 해결
- 복잡한 설정
- 성능 문제
- 명확한 계약 부족
1.3 Federation의 등장
Apollo Federation v1 (2019): Schema stitching의 진화. Federation v2 (2022): 더 명확한 디자인, 더 강력.
[Apollo Router]
/ | \
[Users] [Orders] [Products] ← 각 subgraph가 독립
핵심: 각 팀이 자기 도메인의 GraphQL 서비스 운영. Router가 자동 합성.
2. Federation 핵심 개념
2.1 Subgraph
Subgraph = 페더레이션의 단위. 자체 GraphQL 스키마.
# users-subgraph
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}
@key: 이 타입을 다른 subgraph에서 참조 가능하게 만듦.
2.2 Entity
Entity = 여러 subgraph 간 공유되는 타입. @key로 표시.
users-subgraph:
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
orders-subgraph:
type Order @key(fields: "id") {
id: ID!
total: Float!
user: User! # User entity 참조
}
# User를 확장 (extend가 v1, v2에서는 그냥 정의)
type User @key(fields: "id") {
id: ID! @external
orders: [Order!]! # 새 필드 추가
}
→ 같은 User 타입에 두 subgraph가 다른 필드 제공.
2.3 Reference Resolver
다른 subgraph가 entity를 참조할 때, 해당 entity의 데이터를 가져오는 방법:
// users-subgraph
const resolvers = {
User: {
__resolveReference(reference) {
// reference = { id: "123" }
return getUserById(reference.id)
}
}
}
Router가 { id: "123" }만 보내면, users-subgraph가 전체 User 데이터를 반환.
2.4 Composition
Router는 모든 subgraph 스키마를 받아 supergraph schema를 생성.
Subgraph A schema ─┐
Subgraph B schema ─┼→ [Composition] → Supergraph schema
Subgraph C schema ─┘
클라이언트는 단일 supergraph 스키마만 봅니다.
3. 실전 예시 — 전자상거래
3.1 Subgraph 설계
3개의 subgraph:
1. Users
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
address: Address!
}
type Address {
street: String!
city: String!
zipcode: String!
}
type Query {
user(id: ID!): User
me: User
}
2. Products
type Product @key(fields: "id") {
id: ID!
name: String!
description: String!
price: Float!
stock: Int!
}
type Query {
product(id: ID!): Product
products(category: String): [Product!]!
}
3. Reviews
type Review @key(fields: "id") {
id: ID!
rating: Int!
comment: String!
productId: ID!
authorId: ID!
}
# Product 확장
type Product @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
averageRating: Float!
}
# User 확장
type User @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
}
3.2 클라이언트 쿼리
query {
user(id: "123") {
name # users-subgraph
email # users-subgraph
reviews { # reviews-subgraph
rating
comment
product { # products-subgraph
name
price
}
}
}
}
Router가 자동으로:
- users-subgraph:
name,email가져옴 - reviews-subgraph: User의 reviews 가져옴
- products-subgraph: 각 review의 product 정보 가져옴
클라이언트는 단일 쿼리, 단일 응답을 받습니다.
3.3 실행 흐름
[Client] → [Router]
↓
├─ users-subgraph: query user(id: 123)
│ ↓
│ { name, email, reviews: ??? }
│
├─ reviews-subgraph:
│ __resolveReference: User { id: 123 }
│ → return reviews
│
└─ products-subgraph:
__resolveReference: Product { id: 456 }
→ return product details
[Router] → [Composes response] → [Client]
4. Apollo Router — 새로운 표준
4.1 Apollo Gateway → Router 마이그레이션
Apollo Gateway (Node.js, 2019):
- Federation의 첫 구현
- Node.js 기반
- 단일 스레드, 메모리 사용 많음
Apollo Router (Rust, 2022):
- Rust로 다시 작성
- 10배 이상 빠름
- 메모리 효율
- 더 적은 인스턴스로 같은 트래픽 처리
4.2 Router 설치
# 다운로드
curl -sSL https://router.apollo.dev/download/nix/latest | sh
# 실행
./router --config router.yaml --supergraph supergraph.graphql
router.yaml:
supergraph:
listen: 0.0.0.0:4000
cors:
origins:
- https://example.com
telemetry:
metrics:
prometheus:
enabled: true
tracing:
otlp:
enabled: true
endpoint: http://otel-collector:4317
4.3 Schema Composition
각 subgraph 변경 시 supergraph 재생성:
# Rover CLI
rover supergraph compose --config supergraph-config.yaml > supergraph.graphql
supergraph-config.yaml:
federation_version: 2
subgraphs:
users:
routing_url: http://users-service:4001
schema:
subgraph_url: http://users-service:4001
products:
routing_url: http://products-service:4002
schema:
subgraph_url: http://products-service:4002
reviews:
routing_url: http://reviews-service:4003
schema:
subgraph_url: http://reviews-service:4003
4.4 GraphOS — Apollo의 매니지드
Apollo Studio / GraphOS:
- Schema registry (자동 composition)
- Schema validation (CI 통합)
- Operation 분석
- Performance 모니터링
- Schema diff (브레이킹 체인지 감지)
# CI에서 schema validation
rover subgraph check my-graph@prod \
--schema users.graphql \
--name users
5. Federation 디렉티브
5.1 @key
Entity 정의:
type User @key(fields: "id") {
id: ID!
name: String!
}
# 복합 키
type Order @key(fields: "id userId") {
id: ID!
userId: ID!
total: Float!
}
# 다중 key
type Product
@key(fields: "id")
@key(fields: "sku") {
id: ID!
sku: String!
name: String!
}
5.2 @external
다른 subgraph가 정의한 필드를 참조:
type User @key(fields: "id") {
id: ID! @external
reviews: [Review!]! # 새 필드
}
5.3 @requires
다른 subgraph의 필드가 필요:
type Product @key(fields: "id") {
id: ID! @external
weight: Float! @external
shippingCost: Float! @requires(fields: "weight")
}
shippingCost를 계산하려면 weight가 필요. Router가 자동으로 weight를 가져와 전달.
5.4 @provides
이 subgraph가 제공할 수 있는 필드:
type Review @key(fields: "id") {
id: ID!
product: Product! @provides(fields: "name")
}
type Product @key(fields: "id") {
id: ID! @external
name: String! @external
}
→ Reviews subgraph가 product name까지 직접 반환 → product subgraph 호출 절약.
5.5 @shareable (v2)
여러 subgraph가 같은 필드를 정의:
# users-subgraph
type User @key(fields: "id") {
id: ID!
name: String! @shareable
}
# auth-subgraph
type User @key(fields: "id") {
id: ID!
name: String! @shareable # 같은 필드, 둘 다 정의 가능
}
5.6 @inaccessible
특정 필드를 supergraph에서 숨김:
type User @key(fields: "id") {
id: ID!
name: String!
internalNotes: String! @inaccessible # supergraph에 노출 안 됨
}
6. 흔한 패턴
6.1 도메인 분할
잘못된 분할: 기술별
- Database subgraph
- Cache subgraph
- Auth subgraph
올바른 분할: 비즈니스 도메인별
- Users subgraph
- Orders subgraph
- Products subgraph
- Inventory subgraph
- Shipping subgraph
→ 각 팀이 자기 도메인 책임.
6.2 Entity 소유권
각 entity는 하나의 "owner" subgraph가 있어야 합니다.
소유: 해당 entity의 정체성을 정의하는 곳.
User entity → users-subgraph가 소유
- id, name, email (소유 필드)
- orders-subgraph: User에 orders 필드 추가
- reviews-subgraph: User에 reviews 필드 추가
6.3 N+1 방지
query {
users {
name
orders {
id
}
}
}
잘못된 구현: 각 user마다 별도 호출 → N+1.
해결: DataLoader 패턴.
const orderLoader = new DataLoader(async (userIds) => {
// 한 번의 쿼리로 여러 user의 orders 가져옴
const orders = await db.query('SELECT * FROM orders WHERE user_id IN (?)', userIds)
return userIds.map(id => orders.filter(o => o.user_id === id))
})
const resolvers = {
User: {
orders: (user) => orderLoader.load(user.id)
}
}
6.4 인증/인가
Router 레벨:
authentication:
router:
jwt:
jwks:
- url: https://auth.example.com/.well-known/jwks.json
Subgraph로 컨텍스트 전달:
headers:
all:
request:
- propagate:
named: authorization
→ Router가 JWT를 검증하고 subgraph로 전달.
7. 운영과 모니터링
7.1 메트릭
Apollo Router는 Prometheus 메트릭 기본 지원:
apollo_router_http_requests_total
apollo_router_http_request_duration_seconds
apollo_router_operations_total
apollo_router_session_count
대시보드: Grafana로 시각화.
7.2 분산 트레이싱
telemetry:
tracing:
otlp:
enabled: true
endpoint: http://otel-collector:4317
propagation:
trace_context: true
jaeger: true
→ Router → Subgraph 호출이 같은 trace에 포함.
Jaeger UI:
[Router] (50ms)
├─ [users-subgraph] (10ms)
├─ [orders-subgraph] (30ms)
│ └─ [DB query] (25ms) ← 병목
└─ [Composition] (10ms)
7.3 Schema Registry
왜 필요한가?: Subgraph 변경이 다른 subgraph를 망가뜨릴 수 있음.
# CI에서 변경 검증
rover subgraph check my-graph@prod \
--schema new-schema.graphql \
--name users
검출:
- Breaking change (필드 제거)
- Composition 충돌 (다른 subgraph와 모순)
- 사용량 경고
7.4 Persisted Queries
보안 + 성능:
- 클라이언트가 쿼리 ID만 전송
- Router는 미리 등록된 쿼리만 실행
// Persisted query 등록
const queryId = "abc123"
const query = "query GetUser($id: ID!) { user(id: $id) { name } }"
// 클라이언트
fetch('/graphql', {
body: JSON.stringify({
extensions: {
persistedQuery: {
version: 1,
sha256Hash: queryId
}
},
variables: { id: "123" }
})
})
효과:
- 임의 쿼리 차단 (보안)
- 작은 페이로드 (네트워크 절약)
- 사전 분석 (성능 모니터링)
8. 마이그레이션 전략
8.1 모놀리식 GraphQL → Federation
1단계: Schema 분석
# 기존 모놀리스
type Query {
user(id: ID!): User
product(id: ID!): Product
order(id: ID!): Order
}
도메인별로 분류.
2단계: 첫 subgraph 추출
- 가장 명확한 도메인부터 (예: users)
- 모놀리스는 그대로 두고 새 subgraph 추가
- Router를 모놀리스 앞에
3단계: 점진적 분리
- 한 도메인씩 모놀리스에서 추출
- 모놀리스의 해당 부분 제거
4단계: 모놀리스 제거
- 모든 도메인이 분리되면 모놀리스 폐기
8.2 REST → Federation
REST API를 GraphQL로 감싸기:
Step 1: 각 REST API를 GraphQL subgraph로 래핑
const resolvers = {
Query: {
user: async (_, { id }) => {
const response = await fetch(`http://users-rest/users/${id}`)
return response.json()
}
}
}
Step 2: 여러 wrapper subgraph를 federate
Step 3: 점진적으로 native GraphQL로 전환
8.3 일반적 함정
1. 너무 작게 분할
- 5명짜리 도메인을 10개 subgraph로 → 운영 지옥
- Conway's Law: 팀 구조에 맞추기
2. Entity ownership 모호
- 두 subgraph가 같은 entity 소유 주장 → 충돌
- 명확한 owner 결정
3. 동기 의존성
- A subgraph가 B에 동기적으로 의존 → 캐스케이드 실패
- DataLoader, 캐싱
9. 실제 사례
9.1 Netflix
- 수백 개 subgraph
- Java + DGS (Domain Graph Service) framework
- Schema-first 접근
- Backend for Frontend (BFF) 패턴
9.2 Airbnb
- 모놀리스 → Federation 마이그레이션
- 중요한 발견: 점진적 마이그레이션의 중요성
- 한 번에 하지 말 것
9.3 Expedia
- 여행 검색의 거대 시스템
- 50+ subgraph
- DataLoader 패턴 광범위 사용
9.4 GitHub
- Public GraphQL API (federation 사용)
- 외부 개발자 친화적
10. Federation vs 대안
10.1 GraphQL 모놀리스 vs Federation
| 모놀리스 | Federation | |
|---|---|---|
| 복잡도 | 낮음 | 높음 |
| 팀 자율성 | 낮음 | 높음 |
| 확장성 | 단일 노드 | 무한 |
| 개발 속도 | 작은 팀 빠름 | 큰 팀 빠름 |
| 운영 | 단순 | 복잡 |
선택: 팀 5명 미만 → 모놀리스. 그 이상 → Federation 고려.
10.2 Federation vs BFF
BFF (Backend for Frontend):
- 클라이언트별 GraphQL 서버 (web, mobile, partner)
- 각 BFF가 여러 서비스 호출
Federation:
- 단일 supergraph
- 클라이언트 무관
조합: Federation + 클라이언트별 view (Apollo client preset).
10.3 Federation vs gRPC
gRPC: 강한 타입, 빠름. 하지만 클라이언트(특히 web)가 직접 사용 어려움.
Federation: GraphQL 유연성. 내부적으로 gRPC 가능.
일반 패턴:
- 클라이언트 ↔ Apollo Router (GraphQL)
- Router ↔ Subgraph (GraphQL)
- Subgraph ↔ 내부 서비스 (gRPC)
퀴즈
1. Schema Stitching과 Federation의 차이는?
답: Schema Stitching (이전): 여러 GraphQL 스키마를 합쳐 하나로 제공. 수동 충돌 해결, 복잡한 설정, 명확한 계약 부족. Federation (Apollo): subgraph 간 명시적 계약 (@key, @external). Composition이 자동, breaking change 자동 검출. Federation v2가 stitching의 모든 단점을 해결하여 사실상 표준이 되었습니다. Stitching은 deprecated.
2. @key 디렉티브의 역할은?
답: Entity를 정의합니다. @key(fields: "id")는 "이 타입은 id 필드로 식별 가능하며, 다른 subgraph에서 참조 가능"을 의미. Router가 entity reference를 만날 때 id로 해당 subgraph에 데이터를 요청합니다 (__resolveReference). 복합 key (@key(fields: "id userId")) 또는 다중 key도 가능. Federation의 가장 중요한 디렉티브.
3. Apollo Router가 Apollo Gateway보다 빠른 이유는?
답: Rust로 다시 작성되었기 때문입니다. Apollo Gateway는 Node.js 기반 (단일 스레드, 메모리 많이 사용). Apollo Router는 (1) Rust의 zero-cost abstraction, (2) async runtime (tokio)로 진정한 멀티스레드, (3) 메모리 효율. 결과: 10배 이상 빠르고, 메모리 절반 이하. 같은 트래픽에 더 적은 인스턴스. Apollo Gateway는 deprecated.
4. Subgraph 분할의 올바른 기준은?
답: 비즈니스 도메인별로 분할합니다. 잘못된 예: Database, Cache, Auth (기술별). 올바른 예: Users, Orders, Products, Inventory, Shipping (도메인별). Conway's Law를 따라 팀 구조와 일치시키는 것이 중요. 한 팀이 한 subgraph를 책임지면 자율성 + 빠른 개발. 너무 작게 분할(5명짜리 도메인 → 10개)하지 말고, 너무 크게 분할(50명짜리 단일 subgraph)하지도 말 것.
5. Federation에서 N+1을 어떻게 방지하나요?
답: DataLoader 패턴을 사용합니다. 같은 tick에 여러 entity 요청을 모아 한 번의 쿼리로 처리. Router는 entity reference를 batch로 보내고, subgraph는 DataLoader로 한 번에 처리. 또한 (1) @provides 디렉티브로 cross-subgraph 호출 자체를 줄이거나, (2) 캐싱 (Redis), (3) GraphQL operation 분석으로 자주 함께 요청되는 entity를 식별하여 최적화. DataLoader는 GraphQL의 필수 패턴.
참고 자료
- Apollo Federation Docs
- Apollo Router
- GraphOS
- Federation v2 Spec
- DataLoader
- Netflix DGS — Java framework
- Hot Chocolate — .NET GraphQL with federation
- Mercurius — Node.js with federation
- GraphQL Schema Design — Apollo's design principles
- How Netflix Scales GraphQL
- Airbnb GraphQL Migration