Apollo Federation 2.0을 활용한 마이크로서비스 GraphQL 아키텍처를 다룹니다. 서브그래프와 슈퍼그래프 개념, 엔티티 참조 리졸버, 주요 디렉티브, 스키마 컴포지션 과정을 학습합니다.
@key, @external, @provides, @requires 디렉티브의 용도를 파악한다조직이 성장하면 하나의 모놀리식 GraphQL 서버로는 한계에 부딪힙니다.
Apollo Federation은 이 문제를 해결합니다. 각 팀이 자신의 도메인에 해당하는 서브그래프(Subgraph) 를 독립적으로 개발하고 배포하며, 라우터(Router) 가 이를 하나의 슈퍼그래프(Supergraph) 로 통합합니다.
클라이언트는 라우터의 단일 엔드포인트만 알면 됩니다. 내부적으로 어떤 서브그래프에서 데이터를 가져오는지는 라우터가 처리합니다.
엔티티는 여러 서브그래프에 걸쳐 존재하는 공유 타입입니다. @key 디렉티브로 식별자를 지정합니다.
# Users 서브그래프 — User 엔티티의 소유자
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
avatar: String
}
type Query {
user(id: ID!): User
me: User
}# Reviews 서브그래프 — User 엔티티를 확장
type User @key(fields: "id") {
id: ID!
reviews: [Review!]! # 이 서브그래프가 추가하는 필드
}
type Review @key(fields: "id") {
id: ID!
rating: Int!
body: String!
author: User!
product: Product!
}User 타입이 두 서브그래프에 모두 존재합니다. Users 서브그래프는 name, email, avatar를, Reviews 서브그래프는 reviews를 책임집니다. 라우터가 이를 하나로 합칩니다.
서브그래프가 다른 서브그래프의 엔티티를 참조할 때, 해당 엔티티의 데이터를 가져오기 위해 참조 리졸버가 필요합니다.
// Users 서브그래프
const resolvers = {
User: {
__resolveReference: async (
reference: { __typename: 'User'; id: string },
context: Context,
) => {
// 4장에서 배운 DataLoader 사용이 필수
return context.loaders.userById.load(reference.id);
},
},
};라우터가 Reviews 서브그래프에서 Review.author를 조회한 후, 해당 User의 name이나 email이 필요하면 Users 서브그래프의 참조 리졸버를 호출합니다.
클라이언트가 다음 쿼리를 보내면 라우터가 어떻게 처리하는지 살펴보겠습니다.
query {
me {
name # Users 서브그래프
email # Users 서브그래프
reviews { # Reviews 서브그래프
rating
body
product { # Products 서브그래프
name
price
}
}
}
}Federation 2.0에서 사용하는 핵심 디렉티브를 정리합니다.
엔티티의 식별자를 지정합니다. 복합 키도 가능합니다.
# 단일 키
type User @key(fields: "id") {
id: ID!
name: String!
}
# 복합 키
type ProductVariant @key(fields: "productId sku") {
productId: ID!
sku: String!
price: Int!
}
# 다중 키 (여러 방법으로 식별 가능)
type User @key(fields: "id") @key(fields: "email") {
id: ID!
email: String!
name: String!
}다른 서브그래프에서 정의된 필드를 참조할 때 사용합니다.
# Reviews 서브그래프
type Product @key(fields: "id") {
id: ID!
averageRating: Float! @requires(fields: "reviewCount totalRating")
reviewCount: Int! @external
totalRating: Int! @external
}서브그래프가 엔티티의 특정 필드를 함께 제공할 수 있음을 선언합니다. 라우터가 추가 서브그래프 호출을 줄이는 최적화에 사용됩니다.
# Reviews 서브그래프
type Review @key(fields: "id") {
id: ID!
body: String!
author: User! @provides(fields: "name")
}
# Reviews 서브그래프가 User.name을 직접 제공할 수 있음
type User @key(fields: "id") {
id: ID!
name: String! @external
}이 경우 Review.author.name만 필요한 쿼리에서는 라우터가 Users 서브그래프를 호출하지 않아도 됩니다.
필드 해결에 다른 서브그래프의 필드가 필요함을 선언합니다.
# Shipping 서브그래프
type Product @key(fields: "id") {
id: ID!
weight: Float! @external
dimensions: String! @external
shippingCost: Float! @requires(fields: "weight dimensions")
}shippingCost를 계산하려면 Products 서브그래프의 weight와 dimensions가 필요합니다. 라우터가 자동으로 Products 서브그래프에서 이 데이터를 먼저 가져옵니다.
Federation을 지원하는 서브그래프 서버를 구현합니다.
import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { parse } from 'graphql';
import { readFileSync } from 'fs';
const typeDefs = parse(readFileSync('./schema.graphql', 'utf8'));
const resolvers = {
Query: {
user: async (_parent: unknown, args: { id: string }, context: Context) => {
return context.dataSources.users.findById(args.id);
},
me: async (_parent: unknown, _args: unknown, context: Context) => {
if (!context.currentUser) return null;
return context.dataSources.users.findById(context.currentUser.id);
},
},
User: {
__resolveReference: async (
ref: { __typename: 'User'; id: string },
context: Context,
) => {
return context.loaders.userById.load(ref.id);
},
},
};
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers }),
});buildSubgraphSchema는 일반 ApolloServer에서 사용하는 typeDefs + resolvers 대신 Federation 사양에 맞는 스키마를 생성합니다. _entities 쿼리와 _service 쿼리가 자동으로 추가됩니다.
여러 서브그래프의 스키마를 하나의 슈퍼그래프로 합치는 과정이 스키마 컴포지션(Schema Composition) 입니다.
개발 환경에서 Rover CLI를 사용하여 로컬에서 컴포지션을 수행할 수 있습니다.
federation_version: =2.9
subgraphs:
users:
routing_url: http://localhost:4001/graphql
schema:
file: ./subgraphs/users/schema.graphql
products:
routing_url: http://localhost:4002/graphql
schema:
file: ./subgraphs/products/schema.graphql
reviews:
routing_url: http://localhost:4003/graphql
schema:
file: ./subgraphs/reviews/schema.graphql# Rover CLI로 슈퍼그래프 스키마 생성
rover supergraph compose --config ./supergraph.yaml > supergraph.graphql
# 컴포지션 검증 (CI에서 사용)
rover supergraph compose --config ./supergraph.yaml --dry-run프로덕션 환경에서는 Apollo Studio를 통한 관리형 페더레이션을 권장합니다.
# 서브그래프 스키마 변경 검증 (CI에서 실행)
rover subgraph check my-graph@production \
--name users \
--schema ./schema.graphql
# 서브그래프 스키마 배포 (CD에서 실행)
rover subgraph publish my-graph@production \
--name users \
--schema ./schema.graphql \
--routing-url https://users.internal:4001/graphqlrover subgraph check는 Breaking Change를 감지합니다. 기존 클라이언트가 사용 중인 필드를 삭제하거나 타입을 변경하면 경고를 발생시킵니다. 이 단계를 CI 파이프라인에 포함시켜 안전한 스키마 진화를 보장해야 합니다.
Apollo Router는 Rust로 작성된 고성능 GraphQL 게이트웨이입니다.
supergraph:
listen: 0.0.0.0:4000
# 헤더 전파 설정
headers:
all:
request:
- propagate:
matching: "authorization|x-request-id|x-trace-id"
# CORS 설정
cors:
origins:
- https://app.example.com
allow_headers:
- Content-Type
- Authorization
# 텔레메트리
telemetry:
instrumentation:
spans:
mode: spec_compliant
exporters:
tracing:
otlp:
enabled: true
endpoint: http://otel-collector:4317
# 쿼리 제한
limits:
max_depth: 15
max_height: 200
max_aliases: 30효과적인 Federation 아키텍처를 위한 핵심 원칙을 정리합니다.
엔티티 소유권을 명확히 하라: 각 엔티티에는 하나의 "소유자" 서브그래프가 있어야 합니다. 소유자가 핵심 필드를 정의하고, 다른 서브그래프는 필드를 확장합니다.
서브그래프 경계는 도메인을 따르라: 기술적 레이어(auth, logging)가 아닌 비즈니스 도메인(사용자, 상품, 주문)으로 서브그래프를 나누어야 합니다.
공유 타입을 남용하지 마라: 모든 타입을 엔티티로 만들면 서브그래프 간 결합도가 높아집니다. 진정으로 여러 도메인에 걸친 타입만 엔티티로 정의합니다.
참조 리졸버에서 DataLoader를 사용하라: 4장에서 강조했듯이, 참조 리졸버에서 DataLoader 없이는 N+1 문제가 서브그래프 간 네트워크 호출 수준으로 증폭됩니다.
이번 장에서는 Apollo Federation 2.0을 활용한 분산 GraphQL 아키텍처를 학습했습니다. 서브그래프와 슈퍼그래프의 개념, 엔티티와 참조 리졸버의 동작 원리, @key, @external, @provides, @requires 디렉티브의 용도를 파악했습니다. 스키마 컴포지션과 관리형 페더레이션을 통한 안전한 스키마 진화 방법도 살펴보았습니다.
Federation은 대규모 조직에서 GraphQL의 장점을 극대화하는 아키텍처 패턴입니다. 각 팀이 독립적으로 개발하면서도 클라이언트에게는 하나의 통합된 그래프를 제공할 수 있습니다.
6장에서는 GraphQL의 세 번째 루트 타입인 Subscription을 다룹니다. WebSocket 기반 실시간 데이터 구독의 구현, PubSub 패턴, Redis를 활용한 확장, 그리고 Federation 환경에서의 구독 한계와 대안 패턴을 살펴보겠습니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
GraphQL Subscriptions의 프로토콜과 구현을 다룹니다. WebSocket 전송, PubSub 패턴, Redis를 활용한 확장, 필터링 전략, 그리고 Federation 환경에서의 한계와 SSE 등 대안 패턴을 살펴봅니다.
GraphQL에서 가장 흔한 성능 문제인 N+1 문제의 원인을 분석하고, DataLoader를 통한 배칭과 캐싱 최적화를 구현합니다. 쿼리 복잡도 분석과 깊이 제한 전략도 다룹니다.
GraphQL API에서의 인증과 인가 전략을 다룹니다. JWT 인증 미들웨어, 컨텍스트 기반 사용자 주입, 리졸버 수준 인가, 디렉티브 기반 선언적 인가, RBAC/ABAC 모델을 학습합니다.