GraphQL이란?
GraphQL은 Facebook이 2012년에 내부적으로 개발하고 2015년에 오픈소스로 공개한 API 쿼리 언어이자 런타임이다. 클라이언트가 필요한 데이터의 구조를 직접 명시하여 요청하고, 서버는 정확히 그 구조대로 응답한다.
REST와 달리 단일 엔드포인트(/graphql)로 모든 데이터 요청을 처리하며, 클라이언트가 응답의 형태를 결정한다는 점이 핵심 차이다.
REST와의 비교
| 항목 | REST | GraphQL |
|---|---|---|
| 엔드포인트 | 리소스별 다수 (/users, /orders) | 단일 (/graphql) |
| 데이터 결정 주체 | 서버가 응답 구조를 결정 | 클라이언트가 필요한 필드를 명시 |
| Over-fetching | 불필요한 필드까지 반환 | 요청한 필드만 반환 |
| Under-fetching | 연관 데이터 조회 시 추가 요청 필요 | 한 번의 쿼리로 중첩 데이터 조회 |
| 버전 관리 | URL 버전(/v1/, /v2/) | 스키마 진화로 버전 없이 운영 가능 |
| 캐싱 | HTTP 캐싱 자연스러움 | 별도 캐싱 전략 필요 (Persisted Query 등) |
| 타입 시스템 | OpenAPI/Swagger로 별도 정의 | 스키마 자체가 타입 시스템 |
핵심 개념
Schema
GraphQL API의 계약서다. 서버가 제공하는 모든 타입, 쿼리, 뮤테이션을 SDL(Schema Definition Language)로 정의한다.
type User {
id: ID!
name: String!
email: String!
orders: [Order!]!
}
type Order {
id: ID!
total: Float!
status: OrderStatus!
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
}Query
데이터를 읽는 연산이다. 클라이언트가 필요한 필드만 선택하여 요청한다.
query {
user(id: "123") {
name
orders {
total
status
}
}
}Mutation
데이터를 변경하는 연산이다. 생성, 수정, 삭제에 해당한다.
mutation {
createOrder(input: { userId: "123", items: [...] }) {
id
status
}
}Subscription
서버에서 클라이언트로 실시간 데이터를 푸시하는 연산이다. WebSocket 기반으로 동작한다.
subscription {
orderStatusChanged(orderId: "456") {
status
}
}Type System
GraphQL은 강타입 시스템을 내장하고 있다. 스키마 자체가 API의 타입 정의이자 문서 역할을 한다.
| 타입 | 설명 | 예시 |
|---|---|---|
| Scalar | 원시 타입 | Int, Float, String, Boolean, ID |
| Object | 필드의 집합 | type User { name: String! } |
| Enum | 열거형 | enum Status { ACTIVE, INACTIVE } |
| Input | Mutation 인자용 객체 | input CreateUserInput { name: String! } |
| Interface | 공통 필드 추상화 | interface Node { id: ID! } |
| Union | 여러 타입 중 하나 | union SearchResult = User | Order |
!는 Non-Null을 의미하며, [Type!]!은 “null이 아닌 배열, 요소도 null 아님”을 뜻한다.
Resolver
스키마의 각 필드에 대해 실제 데이터를 반환하는 함수다. GraphQL 런타임은 쿼리를 파싱한 뒤, 각 필드에 매핑된 Resolver를 호출하여 결과를 조립한다.
Query.user(id) → DB에서 사용자 조회
User.orders → 해당 사용자의 주문 목록 조회
Order.status → 주문 상태 반환
Resolver는 부모 객체의 결과를 인자로 받으므로, 중첩된 필드도 순차적으로 해소된다.
N+1 문제와 DataLoader
GraphQL의 대표적 성능 함정이다. 목록 조회 시 각 항목마다 Resolver가 개별 호출되어, 1번의 목록 쿼리 + N번의 상세 쿼리가 발생한다.
해결 방법 — DataLoader 패턴:
- 같은 틱(tick)에 발생한 개별 요청을 모아서 배치 쿼리 1회로 처리
- Facebook이 GraphQL과 함께 공개한 패턴으로, 대부분의 언어에 구현체가 존재
Introspection
GraphQL 서버는 자기 자신의 스키마를 쿼리할 수 있는 인트로스펙션 기능을 내장한다.
{
__schema {
types { name }
}
__type(name: "User") {
fields { name type { name } }
}
}이를 통해 GraphQL Playground, GraphiQL 같은 도구가 자동 완성과 문서화를 제공한다. 프로덕션 환경에서는 보안상 비활성화하는 것이 일반적이다.
Persisted Query
클라이언트가 매번 전체 쿼리 문자열을 보내는 대신, 사전에 등록된 쿼리의 해시만 전송하는 기법이다.
- 성능: 네트워크 페이로드 감소
- 보안: 허용된 쿼리만 실행 가능 (allowlist)
- 캐싱: CDN 레벨 캐싱이 가능해짐
Federation
여러 GraphQL 서비스의 스키마를 하나의 통합 그래프로 합성하는 아키텍처 패턴이다. 각 서비스가 자신의 도메인에 해당하는 스키마 일부를 소유하고, Gateway가 이를 조합한다.
┌─────────┐
│ Gateway │ ← 클라이언트 요청 수신
└────┬────┘
├── User Service (User 타입 소유)
├── Order Service (Order 타입 소유)
└── Product Service (Product 타입 소유)
MSA 환경에서 각 팀이 독립적으로 스키마를 관리하면서도, 클라이언트에는 단일 API로 제공할 수 있다.
관련 문서
- Relay Specification — Cursor 페이지네이션, Node 인터페이스 등 GraphQL 사실상 표준 규격
- gRPC — 또 다른 API 통신 규격 (RPC 기반)
- protobuf — gRPC의 직렬화 포맷, GraphQL의 SDL과 대비되는 스키마 정의 방식