02. Stitching → Federation 역사 — 2017 → 2019 → 2022
GraphQL의 그래프 통합 진화는 세 세대를 거쳤다. 2017 schema stitching → 2019 Federation v1 → 2022 Federation v2. 각 단계는 이전 단계의 한계에서 출발했다 — 추상화의 흔한 패턴이다.
한 줄 답
Stitching(2017)은 런타임 합성이라 느리고 충돌 해석이 없었다. Federation v1(2019)은 컴파일타임 합성을 도입했지만 shared ownership 표현이 부족했다. Federation v2(2022)는
@shareable·@override로 팀 간 협업의 첫 클래스 시민을 만들었다.
Why — 왜 세대가 바뀌었나
GraphQL이 마이크로서비스 시대에 그래프 통합 문제를 풀어야 했던 건 불가피했다. 회사가 커지면 N개 GraphQL 서버가 생겼고, 클라이언트는 그것들이 한 그래프처럼 보이기를 원했다.
세 세대의 진화는 세 가지 다른 가정을 시험한 결과다.
| 세대 | 시기 | 합성 시점 | 핵심 가정 |
|---|---|---|---|
| Stitching | 2017 | 런타임 | ”gateway가 SDL을 가져와 합치면 된다” |
| Federation v1 | 2019 | 컴파일 (rover compose) | “subgraph가 명시적으로 통합 규칙을 선언해야 한다” |
| Federation v2 | 2022 | 컴파일 + 합성 검증 | ”팀 간 공유 소유권도 첫 클래스로 표현되어야 한다” |
How — 세 세대의 동작 차이
1세대: Schema Stitching (2017~2019)
Apollo가 처음 만든 graphql-tools/stitching. 동작 방식:
// Gateway 코드
import { stitchSchemas } from '@graphql-tools/stitch';
const gateway = stitchSchemas({
subschemas: [
{ schema: userSchema, executor: makeRemoteExecutor('http://user-svc/graphql') },
{ schema: orderSchema, executor: makeRemoteExecutor('http://order-svc/graphql') },
],
typeDefs: `
extend type User {
orders: [Order!]!
}
`,
resolvers: {
User: {
orders: (user, _, ctx, info) => delegateToSchema({
schema: orderSchema,
operation: 'query',
fieldName: 'ordersByUserId',
args: { userId: user.id },
context: ctx, info,
}),
},
},
});→ Gateway가 런타임에 SDL을 가져와 합치고, 충돌은 gateway 코드에 직접 작성한 resolver가 해석한다.
문제점:
- 런타임 비용: gateway가 시작할 때 모든 subschema의 SDL을 fetch해야 함. 부팅이 느리다.
- 충돌 해석을 gateway 코드가 한다: 두 subschema에 동일한 type이 있으면 — gateway 개발자가 직접
transformSchema로 충돌을 풀어야 했다. - 타입 안정성 부재: subschema가 말없이 schema를 바꿔도 gateway는 런타임에야 안다.
- 분산된 책임 → 중앙 집중: 결국 gateway 팀이 모든 통합 로직을 갖게 됨 → 다시 조직 병목.
→ stitching은 기술적으론 동작했지만 조직 문제를 풀지 못했다.
2세대: Federation v1 (2019)
Apollo가 반성하며 발표. 핵심 아이디어:
- 컴파일타임 합성: subgraph 스키마들을 빌드 단계에서 supergraph SDL로 합성. 런타임 fetch 없음.
- subgraph가 통합 규칙을 명시: gateway 코드가 아니라 subgraph SDL의 디렉티브가 통합 의도를 선언.
# user subgraph
type User @key(fields: "id") {
id: ID!
name: String!
}
# order subgraph — User를 *확장*
extend type User @key(fields: "id") {
id: ID! @external
orders: [Order!]!
}→ extend type User가 *“이건 다른 subgraph가 정의한 User를 확장한다”*는 선언. gateway는 SDL을 읽기만 하고, 모든 통합 규칙은 subgraph가 소유한다.
v1의 한계:
extend필수: 같은 type을 동시에 두 subgraph에서 원래 정의할 수 없었다. 누군가는 주 소유자고 나머지는extend.- 공유 필드 표현 못함: e.g.,
User.email을 User 팀과 Identity 팀이 각자 정의하고 동일하다고 보장하는 케이스가 표현 불가. - owner 이전이 위험: A팀이 정의한 필드를 B팀이 가져가려면 deploy를 정확히 동시에 해야 함 — federation은 원자적 마이그레이션을 지원 안 함.
- 합성 에러 메시지가 모호: 두 subgraph가 충돌해도 어느 쪽이 잘못인지 불분명.
3세대: Federation v2 (2022)
Apollo가 대대적 재설계. 핵심 변경:
(a) extend 키워드 제거 — 모든 subgraph가 동등
# user subgraph
type User @key(fields: "id") {
id: ID!
name: String!
}
# order subgraph — extend 안 써도 됨
type User @key(fields: "id") {
id: ID!
orders: [Order!]!
}→ “이건 user subgraph 것”이라는 위계가 사라지고 — 모두가 User type에 기여하는 그림이 됐다.
(b) @shareable — 동일 필드를 여러 subgraph가 정의 가능
# user subgraph
type User @key(fields: "id") {
id: ID!
name: String @shareable
}
# identity subgraph
type User @key(fields: "id") {
id: ID!
name: String @shareable # 같은 의미라고 명시적 합의
}→ 두 팀이 같은 필드를 정의해도 명시적 동의 하에 합성된다.
(c) @override — 점진적 소유권 이전
# A subgraph — 원래 owner
type Product @key(fields: "id") {
id: ID!
price: Float! # A팀 소유
}
# B subgraph — A로부터 가져옴
type Product @key(fields: "id") {
id: ID!
price: Float! @override(from: "A") # 이제 B팀 것
}→ B를 배포하는 순간 router는 B의 price를 우선한다. A는 나중에 안전하게 필드를 제거하면 된다.
(d) @inaccessible — 공개 스키마에서 숨김
type User @key(fields: "id") {
id: ID!
internalScore: Float @inaccessible # supergraph 합성엔 있지만 클라이언트엔 안 보임
}→ 내부 디버깅용 필드를 외부에 노출하지 않으면서 내부 router에선 쓸 수 있다.
(e) Composition 검증 강화
v2는 합성 시 충돌을 자세히 진단한다 — 어느 subgraph, 어느 필드, 왜 충돌인지.
COMPOSITION_ERROR: Field "User.email" is defined in both "user" and "identity"
subgraphs but is not marked @shareable in "identity".
Add @shareable to "identity", or remove the field from one subgraph.→ 대부분의 통합 문제를 컴파일 타임에 잡는다. stitching 시대엔 런타임에야 발견했던 것들.
What — 세대별 비교표
| 항목 | Stitching | Federation v1 | Federation v2 |
|---|---|---|---|
| 합성 시점 | 런타임 | 컴파일 (rover compose) | 컴파일 + 강화된 검증 |
| 충돌 해석 | gateway 코드 | extend 위계 | 디렉티브(@shareable/@override) |
| 공유 필드 | resolver로 해결 | ✗ 표현 불가 | ✓ @shareable |
| 소유권 이전 | gateway 재배포 필요 | ✗ 위험 | ✓ @override (점진적) |
| 합성 에러 | 런타임 ½ 미발견 | 컴파일이지만 모호 | 컴파일 + 정확한 진단 |
| Gateway 구현 | Apollo Server | Apollo Gateway (Node) | Apollo Router (Rust) |
| 권장 시점 | 거의 없음 (legacy) | 신규 프로젝트 권장 안 함 | 권장 |
What-if — 왜 지금 stitching을 쓰는 사람이 있나
| 케이스 | 이유 |
|---|---|
| Apollo 외 GraphQL 서버 통합 | Federation은 Apollo 친화적 SDL을 요구. Hasura, Postgraphile 등이 federation 호환 SDL을 부분만 지원 |
| 한 번에 GraphQL이 아닌 REST 통합 | Mesh(다음 챕터)가 stitching 기반 |
| 변경 빈도가 매우 낮음 | stitching의 런타임 비용이 드물게 일어나는 일이면 무시 가능 |
| 레거시 시스템 유지 | 이미 stitching으로 만든 게 있고 굳이 마이그레이션 비용을 안 쓰는 경우 |
Apollo는 2020년 graphql-tools stitching을 deprecated하지 않았다 — 권장하지 않을 뿐이다. 즉, 여전히 일부 케이스에 정답이다.
Insight — 흥미로운 이야기
“Apollo가 federation을 공짜로 풀었던 이유”
Federation은 처음엔 Apollo Gateway(Node.js)만 무료였고 Apollo Studio(supergraph 관리 SaaS)가 유료였다. 2021년 Apollo는 Router를 Rust로 다시 짜고 완전 오픈소스로 풀었다. 이유: grpc로 도망가는 트래픽을 막아야 했고, competitor(Mesh, Hasura)에 표준을 뺏기지 않으려면 federation을 de facto 표준으로 만들어야 했다.
그 결정 덕에 federation이 오픈 스펙(specs.apollo.dev/federation)으로 풀렸고, Hot Chocolate Fusion(.NET) 같은 호환 구현이 나왔다.
“왜 v1 → v2 마이그레이션이 쉬웠는가”
Apollo는 v2를 backward compatible하게 설계했다 — v1 subgraph는 그대로 v2 supergraph에 합성된다. 단, v2 디렉티브를 쓰려면 schema 상단에 *
@link(url: "https://specs.apollo.dev/federation/v2.0", ...)*를 선언해야 한다.→ opt-in 진화. 이건 GraphQL spec 전체의 철학이기도 하다.
“Schema stitching이 영원히 살아있는 이유”
Mesh, WunderGraph, Hasura의 remote schema 기능이 모두 stitching의 사상적 후예다. legacy 시스템 통합이라는 그래프-아닌 세계와의 다리 역할은 federation이 풀지 못한다.
요약 + Mermaid
Stitching(2017) → Federation v1(2019) → Federation v2(2022) 세 세대는 런타임 → 컴파일 → 협업으로 진화했다. 각 단계는 이전의 한계에서 출발했고, 조직 문제를 점점 더 잘 표현하게 됐다. v2가 지금의 사실상 표준이고, 대안은 7번 문서에서 본다.