LabHub

블로그

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

한국어English日本語

TL;DR


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):

Apollo Router (Rust, 2022):

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:

# 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

검출:

7.4 Persisted Queries

보안 + 성능:

// 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 추출

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. 너무 작게 분할

2. Entity ownership 모호

3. 동기 의존성


9. 실제 사례

9.1 Netflix

9.2 Airbnb

9.3 Expedia

9.4 GitHub


10. Federation vs 대안

10.1 GraphQL 모놀리스 vs Federation

모놀리스Federation
복잡도낮음높음
팀 자율성낮음높음
확장성단일 노드무한
개발 속도작은 팀 빠름큰 팀 빠름
운영단순복잡

선택: 팀 5명 미만 → 모놀리스. 그 이상 → Federation 고려.

10.2 Federation vs BFF

BFF (Backend for Frontend):

Federation:

조합: Federation + 클라이언트별 view (Apollo client preset).

10.3 Federation vs gRPC

gRPC: 강한 타입, 빠름. 하지만 클라이언트(특히 web)가 직접 사용 어려움.

Federation: GraphQL 유연성. 내부적으로 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의 필수 패턴.


참고 자료

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다