Skip to content
Published on

GraphQL Federation 완전 가이드 2025: Apollo Federation, Supergraph, 분산 GraphQL

Authors

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] [...]

문제점:

  1. 단일 팀 병목: GraphQL 스키마가 한 곳에서 관리됨
  2. 거대한 코드베이스: 모든 도메인이 한 서버
  3. 배포 결합: 한 도메인 변경 = 전체 재배포
  4. 확장성: 단일 장애점

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가 자동으로:

  1. users-subgraph: name, email 가져옴
  2. reviews-subgraph: User의 reviews 가져옴
  3. 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의 필수 패턴.


참고 자료