수십 개의 마이크로서비스로 분리된 백엔드 환경에서 클라이언트가 각 서비스의 엔드포인트를 개별적으로 호출하는 것은 네트워크 지연(Latency)과 데이터 통합 복잡도를 기하급수적으로 증가시킵니다. 단순히 API Gateway를 통해 요청을 라우팅하는 것만으로는 부족하며, 각 서비스가 보유한 데이터 그래프를 논리적으로 하나의 '슈퍼그래프(Supergraph)'로 결합해야 합니다. 이 과정에서 발생하는 스키마 충돌, 엔티티 리졸빙(Entity Resolving) 비용, 그리고 게이트웨이의 쿼리 플래닝(Query Planning) 부하가 엔지니어링의 핵심 과제입니다.
스키마 스티칭(Stitching)의 한계와 페더레이션의 등장
과거의 '스키마 스티칭' 방식은 게이트웨이에서 모든 스키마의 병합 로직을 중앙 집중적으로 관리해야 했습니다. 이는 서비스 팀이 변경사항을 배포할 때마다 게이트웨이 팀과 조율해야 하는 병목 현상을 초래했습니다. 반면, GraphQL Federation(특히 Apollo Federation V2)은 선언적(Declarative) 구성 모델을 따릅니다.
각 하위 서비스(Subgraph)는 자신이 담당하는 타입과 필드만을 정의하며, 게이트웨이(Router)는 이들의 메타데이터를 수집하여 실행 가능한 통합 스키마를 동적으로 구성합니다. 핵심은 _entities 쿼리와 @key 디렉티브를 통한 분산 엔티티 확장입니다.
페더레이션 구조에서 잘못된 @provides나 @requires 사용은 게이트웨이가 하위 서비스로 수천 개의 병렬 요청을 보내는 N+1 문제를 유발할 수 있습니다. 쿼리 플랜(Query Plan)을 사전에 검증하지 않으면 네트워크 오버헤드로 인해 전체 응답 속도가 모놀리스 대비 2~3배 느려질 수 있습니다.
엔티티(Entity) 확장과 서브그래프 설계
페더레이션의 가장 강력한 기능은 서로 다른 서비스가 동일한 타입(Type)을 공유하고 확장할 수 있다는 점입니다. 예를 들어, User 서비스는 사용자의 기본 정보를 가지고 있고, Review 서비스는 해당 사용자가 작성한 리뷰를 가지고 있을 때, Review 서비스는 User 타입을 '확장'하여 필드를 추가할 수 있습니다.
다음은 Apollo Federation V2 명세를 따르는 서브그래프 스키마 정의 예제입니다.
# [Subgraph A] User Service
# @key를 사용하여 이 타입이 'id'로 식별됨을 선언
type User @key(fields: "id") {
id: ID!
username: String!
email: String
}
type Query {
me: User
}
# ---------------------------------------------------
# [Subgraph B] Review Service
# User 타입은 외부(User Service)에서 정의되었음을 @external로 명시하지 않아도
# V2에서는 @key만으로 확장이 가능함 (Stub Type)
type User @key(fields: "id") {
id: ID!
# 이 서비스에서 User 타입에 reviews 필드를 추가
reviews: [Review]
}
type Review {
id: ID!
body: String
author: User
}
쿼리 플래닝(Query Planning)과 실행 최적화
클라이언트가 { me { reviews { body } } }와 같은 쿼리를 요청하면, 게이트웨이(Router)는 이를 실행하기 위한 Query Plan을 수립합니다. 이 과정은 데이터베이스의 실행 계획(Execution Plan)과 유사합니다.
- Step 1: User Service에
me { id }를 요청하여 사용자 ID를 획득. - Step 2: 획득한 ID들을 사용하여 Review Service에
_entities쿼리를 배치(Batch) 전송. - Step 3: 결과를 병합하여 클라이언트에 반환.
이때 중요한 것은 Apollo Router(Rust 기반)와 같은 고성능 게이트웨이를 사용하는 것입니다. Node.js 기반의 구형 Gateway는 이벤트 루프 블로킹 문제로 인해 높은 처리량(Throughput) 환경에서 병목이 될 수 있습니다.
분산된 그래프 환경에서는 각 서브그래프의 데이터 배포 시점이 다를 수 있습니다. 스키마 레지스트리(예: Apollo Studio, Rover CLI)를 사용하여 스키마 변경 사항이 모든 서브그래프와 게이트웨이에 안전하게 전파되었는지 확인하는 '스키마 체크(Schema Check)' 파이프라인이 필수적입니다.
아키텍처 비교 분석
기존 모놀리식 접근법과 페더레이션 아키텍처의 트레이드오프를 비교하면 다음과 같습니다.
| 비교 항목 | 모놀리식 GraphQL | Apollo Federation |
|---|---|---|
| 개발 속도 | 초기 빠름, 규모 증가 시 느림 | 초기 설정 복잡, 병렬 개발 용이 |
| 장애 격리 | 전체 시스템 다운 위험 | 개별 서브그래프만 영향 (Partial Failure 처리 가능) |
| 네트워크 비용 | 낮음 (In-process 호출) | 높음 (게이트웨이 <-> 서브그래프 간 홉 발생) |
| 배포 독립성 | 낮음 (Coupled) | 높음 (서비스별 독립 배포 가능) |
실제 구현 시 고려사항: 리졸버 레벨 최적화
서브그래프 구현 시 가장 흔한 실수는 __resolveReference 리졸버에서의 비효율적인 DB 조회입니다. 게이트웨이는 여러 엔티티를 한 번에 조회하기 위해 ID 목록을 배열로 전달합니다. 따라서 반드시 Dataloader 패턴을 적용하여 배치 조회를 수행해야 합니다.
// TypeScript Subgraph Resolver Example
// @key(fields: "id")에 의해 호출되는 참조 리졸버
const UserResolvers = {
User: {
// __resolveReference는 게이트웨이가 엔티티를 병합할 때 호출됨
__resolveReference(userRef, { dataSources }) {
// BAD: return db.findUserById(userRef.id);
// GOOD: Dataloader를 사용하여 배치 처리 (N+1 방지)
return dataSources.userLoader.load(userRef.id);
}
}
};
분산 시스템 디버깅을 위해 클라이언트 요청의 X-Trace-Id 헤더를 게이트웨이가 수신하여, 각 서브그래프 호출 시 그대로 전파하도록 설정해야 합니다. 이를 통해 OpenTelemetry 등의 도구에서 전체 요청의 워터폴(Waterfall) 차트를 분석할 수 있습니다.
결론적으로 GraphQL 페더레이션은 대규모 조직의 API 거버넌스를 위한 필연적인 선택입니다. 하지만 이는 단순한 기술 도입이 아니라, 조직의 구조(Conway's Law)를 반영한 스키마 설계 원칙과 엄격한 CI/CD 파이프라인이 동반되어야만 성공할 수 있는 고난도 아키텍처입니다.
Post a Comment