🔷 GraphQL6. Federation03. Federation v2 기본 — subgraph + supergraph + router

03. Federation v2 기본 — subgraph + supergraph + router

Federation의 첫 인상은 복잡하다. 하지만 “세 레이어, 한 워크플로”만 잡으면 단순하다. 이 문서는 최소한의 동작하는 federation을 한 페이지에 담는다.


한 줄 답

Federation은 세 레이어다 — ① subgraph (각 팀이 갖는 GraphQL 서버 + 자기 타입 + 외부 entity 기여) ② supergraph (모든 subgraph SDL을 자동 합성한 클라이언트가 보는 통합 스키마) ③ router (런타임에 쿼리를 받아 subgraph로 fan-out하고 merge하는 게이트웨이). 워크플로는 subgraph publish → rover compose → router reload 셋이다.


Why — 세 레이어로 나눈 이유

GraphQL 그래프 통합의 핵심 충돌:

  • 자기 서비스 단위로 일하고 싶다 → subgraph
  • 클라이언트하나의 그래프를 보고 싶다 → supergraph
  • 서로 다른 시간서로 다른 정보가 필요하다 → router가 런타임에 조립

이 세 욕망이 분리되어야 서로의 변경이 서로를 막지 않는다. v1과 v2의 큰 차이는 — v2는 모두를 동등하게 본다 (v1은 extend원 소유자 > 확장자 위계가 있었다).


How — 세 레이어 상세

Layer 1: Subgraph

각 팀이 독립적으로 운영하는 GraphQL 서버. 일반 GraphQL 서버와 차이는 federation 디렉티브를 SDL에 쓸 수 있다는 것뿐.

# product subgraph (port 4001)
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.0",
        import: ["@key", "@shareable"])
 
type Product @key(fields: "id") {
  id: ID!
  name: String!
  price: Float!
}
 
type Query {
  productById(id: ID!): Product
  products: [Product!]!
}
# review subgraph (port 4002)
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.0",
        import: ["@key"])
 
type Review {
  id: ID!
  rating: Int!
  body: String!
  product: Product!
}
 
# Product를 "참조만" — review subgraph는 Product의 다른 필드는 모름
type Product @key(fields: "id") {
  id: ID!
  reviews: [Review!]!   # 이 필드만 review subgraph가 기여
}
 
type Query {
  reviewsByProductId(productId: ID!): [Review!]!
}

→ 두 subgraph는 각자의 서버에서 동작. 서로의 존재를 모름. 단, 같은 @key를 선언한 Product합성 가능한 entity가 된다 (자세한 건 04 문서).

Resolver 측면

review subgraph가 Product외부에서 받아왔을 때 해석할 수 있어야 한다.

// review subgraph resolvers
const resolvers = {
  Product: {
    __resolveReference(reference) {
      // reference = { __typename: "Product", id: "p1" }
      // review가 product의 reviews만 안다면 — id만 받아서 그것에 대한 reviews 반환
      return { id: reference.id };
    },
    reviews(product) {
      return reviewsDb.findByProductId(product.id);
    },
  },
};

__resolveReference는 *“이 entity를 너는 어떻게 해석하니?”*에 대한 답. federation의 기본 약속.

Layer 2: Supergraph

모든 subgraph SDL을 합쳐 자동 생성된 단일 스키마. 사람이 손으로 쓰지 않는다.

# supergraph.graphql (rover compose 결과물)
schema @link(url: "https://specs.apollo.dev/link/v1.0")
       @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
{ query: Query }
 
type Product
  @join__type(graph: PRODUCT, key: "id")
  @join__type(graph: REVIEW, key: "id")
{
  id: ID! @join__field(graph: PRODUCT)
  name: String! @join__field(graph: PRODUCT)
  price: Float! @join__field(graph: PRODUCT)
  reviews: [Review!]! @join__field(graph: REVIEW)
}
 
type Review @join__type(graph: REVIEW) {
  id: ID!
  rating: Int!
  body: String!
  product: Product!
}
 
type Query {
  productById(id: ID!): Product @join__field(graph: PRODUCT)
  products: [Product!]! @join__field(graph: PRODUCT)
  reviewsByProductId(productId: ID!): [Review!]! @join__field(graph: REVIEW)
}

@join__field(graph: X)는 *“이 필드는 X subgraph가 해석한다”*는 메타데이터. 클라이언트엔 안 보이고 router만 본다.

→ 클라이언트가 보는 SDL은 위에서 join 디렉티브를 벗긴 것 — 일반 GraphQL 스키마.

Layer 3: Router

Apollo Router(Rust 구현). 역할:

  1. 클라이언트의 쿼리를 받음
  2. supergraph SDL을 기준으로 query plan 생성 — “이 쿼리를 어느 subgraph로 어떻게 쪼개?”
  3. 각 subgraph에 부분 쿼리 발사 (병렬·순차 섞임)
  4. 응답을 원래 쿼리 모양으로 merge
  5. 클라이언트에 반환
Client: { productById(id:"p1") { name, reviews { rating } } }

Router의 query plan:
  Step 1) Product subgraph에:
            { productById(id:"p1") { __typename id name } }
  Step 2) Step 1 결과의 {__typename, id}를 받아서 Review subgraph에:
            { _entities(representations:[{__typename:"Product", id:"p1"}]) {
                ... on Product { reviews { rating } } } }
  Step 3) 두 결과를 merge:
            { productById: { name: "...", reviews: [{ rating: 5 }, ...] } }

→ 자세한 query plan은 06 문서.


What — publish-via-rover 워크플로

Apollo의 사실상 표준 워크플로:

(a) 각 subgraph가 자기 SDL을 publish

# product subgraph 팀이 자기 schema를 GraphOS(또는 self-hosted registry)에 publish
rover subgraph publish my-graph@prod \
  --name product \
  --routing-url https://product-svc.internal/graphql \
  --schema ./product.graphql

→ GraphOS는 registry. publish는 합성을 트리거한다.

(b) GraphOS가 자동 합성

GraphOS는 모든 subgraph SDL을 가져와 supergraph.graphql을 만든다. 충돌이 있으면 publish가 거부된다 (이게 컴파일 타임 검증의 정체).

(c) Router가 최신 supergraph를 자동 fetch

# router.yaml
supergraph:
  source: GraphOS    # GraphOS에서 polling
  poll_interval: 10s

→ supergraph가 바뀌면 router는 zero-downtime reload. subgraph 팀은 router 팀과 협의 없이 변경할 수 있다.

(d) Local 개발 — rover dev

# 각 subgraph를 로컬에서 띄우고 — rover dev가 합성 + router를 한 번에 띄움
rover dev --supergraph-config ./supergraph.yaml

supergraph.yaml어느 subgraph가 어디서 도는지만 적어둔 파일. rover dev가 자동 합성하고 로컬 router를 띄운다.


What-if — 흔한 함정

함정증상원인해결
@key가 두 subgraph에서 다른 필드composition error둘이 같은 entity라는 명세 어긋남두 subgraph가 같은 key 필드를 선언
__resolveReference 안 만듦”Cannot resolve Product reference”entity contribute할 때 필수모든 entity에 __resolveReference
Subgraph가 다른 subgraph의 필드를 정의composition error (shareable 미선언)v2에선 명시적 합의 필요@shareable 둘 다 붙이기 (05 문서)
Local에서는 되는데 prod에서 안 됨network unreachablesubgraph URL이 localhost로 publish됨--routing-url을 정확히
Router가 옛 schema를 들고 있음”Field X does not exist”poll_interval 지연poll_interval 줄이거나 webhook으로

Insight — 흥미로운 이야기

“Apollo Router는 Rust로 다시 짠 이유

초기 Apollo Gateway는 Node.js였다. v8 V8 엔진은 single threadGC pause가 있었다. trafic이 늘면서 gateway가 병목이 됐다 — 모든 트래픽이 통과하는 단일 지점이니까.

2021년 Apollo는 Router를 Rust로 재작성. Tokio 기반 비동기. 결과: p99 latency가 1/10, 메모리 사용량이 1/3. 게이트웨이가 함수 호출 수준으로 빨라졌다.

→ federation의 런타임 비용은 줄어들었지만 0이 되진 않았다. fan-out과 sequential dep은 수학적 비용이라 언어로 풀 수 없다.

“왜 supergraph.graphql을 commit하지 말라고 하는가”

Apollo는 supergraph SDL을 git에 안 올리는 걸 권장한다. 이유: supergraph는 subgraph의 함수다. subgraph 변경 시마다 supergraph가 바뀌고, 둘을 git에서 동시에 일관되게 관리하는 건 cherry-pick 지옥이다.

→ 대신 GraphOS registrysingle source of truth. supergraph는 항상 derived.

“GraphQL spec엔 federation이 없다

Federation은 Apollo가 만든 사용자 정의 디렉티브 + 합성 알고리즘이다. GraphQL spec 본문엔 federation의 ‘F’도 없다. 그런데도 사실상 표준이 된 건 — Apollo가 SDL spec을 공개했고 Hot Chocolate, Mercurius 등이 호환 구현을 만들었기 때문. de facto standard의 교과서적 사례.


요약 + Mermaid

Federation v2는 세 레이어, 한 워크플로다. subgraph가 자기 SDL을 publish → GraphOS가 supergraph를 합성 → router가 그것을 들고 런타임에 쿼리를 분배. 각 레이어는 서로 모르고도 동작한다 — 그게 팀 자율성의 정체.