REST API 는 "한 endpoint = 하나의 응답". 화면에 "user 이름 + post 목록 + 친구 5 명" 을 띄우려면 3-4 round trip 또는 over-fetch. GraphQL 은 "client 가 정확히 원하는 모양을 한 번에 요청" — 한 endpoint, 한 query, 정확한 응답. 어떻게? 이 가이드는 GraphQL 의 schema, resolver tree, 가장 흔한 N+1 문제와 DataLoader 의 해결, mutation / subscription / federation, 그리고 GraphQL 이 과한 시나리오를 정리한다.
REST vs GraphQL — 한 줄 예시
# REST — 여러 endpoint
GET /users/42 → 사용자 정보
GET /users/42/posts → 사용자 post 들
GET /users/42/friends?limit=5 → 친구 5 명
3 round trips + 각 응답에 안 쓰는 필드 다수
# GraphQL — 한 query
POST /graphql
{
user(id: 42) {
name
posts {
title
createdAt
}
friends(limit: 5) {
name
avatar
}
}
}
1 round trip. 응답에 정확히 요청한 필드만.핵심 — Schema First
GraphQL 의 출발점은 schema. SDL (Schema Definition Language):
type User {
id: ID!
name: String!
email: String
posts: [Post!]!
friends(limit: Int): [User!]!
}
type Post {
id: ID!
title: String!
content: String
author: User!
createdAt: DateTime!
}
type Query {
user(id: ID!): User
posts(limit: Int = 10): [Post!]!
}
type Mutation {
createPost(title: String!, content: String): Post!
}Schema 가 contract. Client / server 둘 다 schema 기준으로 작업. ! 는 non-nullable. [Post!]! 는 빈 array 는 가능, null array X.
Resolver — 각 field 의 값 채우는 함수
const resolvers = {
Query: {
user: async (_, {id}) => {
return db.users.findById(id);
},
},
User: {
name: (user) => user.name, // 직접 매핑 (default behavior)
posts: async (user) => {
return db.posts.findByUserId(user.id);
},
friends: async (user, {limit}) => {
return db.friendships.findFriends(user.id, limit);
},
},
Post: {
author: async (post) => {
return db.users.findById(post.userId);
},
},
};각 field 마다 resolver. field 단위로 lazy 실행 — client 가 요청한 field 만 resolver 호출.
Query 의 실행 — resolver tree
Query:
{
user(id: 1) {
name
posts {
title
author {
name
}
}
}
}
Server 실행:
1. Query.user(id: 1) resolver → User{id:1, name:"Alice", ...}
2. User.name → "Alice" (default field resolver, 직접 매핑)
3. User.posts resolver → [Post1, Post2, Post3]
4. 각 Post 마다:
- Post.title → 직접 매핑
- Post.author resolver → User{id:1, name:"Alice"} ← 같은 사용자
- User.name → "Alice"결과 JSON:
{
"data": {
"user": {
"name": "Alice",
"posts": [
{"title": "...", "author": {"name": "Alice"}},
{"title": "...", "author": {"name": "Alice"}},
{"title": "...", "author": {"name": "Alice"}}
]
}
}
}N+1 문제 — GraphQL 의 가장 흔한 함정
Query:
{
posts { ← 1 query
title
author { name } ← 각 post 마다 1 query → 10 posts = 10 queries
}
}
순진한 실행:
1. db.posts.findAll() → 10 posts
2. for each post:
db.users.findById(p.userId) → 10 queries
Total: 11 queries (1 + N)
수천 post 면 수천 queries → DB 폭주DataLoader — batching + caching
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (userIds) => {
// 한 번에 fetch
const users = await db.users.findByIds(userIds);
// userIds 순서대로 정렬
return userIds.map(id => users.find(u => u.id === id));
});
// resolver 에서
Post: {
author: async (post) => {
return userLoader.load(post.userId);
// 같은 tick 의 모든 load 호출이 한 번에 batch
},
},
결과:
1. db.posts.findAll() → 10 posts
2. userLoader.load 호출 10 번 → DataLoader 가 batch → 1 query
db.users.findByIds([1,2,3,...]) → 10 users
Total: 2 queriesDataLoader = micro-task 안 모든 load(id) 호출 모아 한 번에 batch. + memoization (같은 id 의 두 번째 load 는 cache).
Mutation — write 작업
# Schema
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
}
input CreatePostInput {
title: String!
content: String
tags: [String!]
}
# Client query
mutation {
createPost(input: {title: "Hello", content: "..."}) {
id
title
createdAt
}
}
# Server response
{
"data": {
"createPost": {
"id": "abc123",
"title": "Hello",
"createdAt": "2026-05-23T..."
}
}
}Mutation 도 query 와 같은 endpoint. Query 와 다른 점 — server 가 sequential 실행 보장 (Query 는 parallel 가능).
Subscription — 실시간 push
# Schema
type Subscription {
postCreated: Post!
}
# Client
subscription {
postCreated {
id
title
author { name }
}
}
# Server
pubsub.publish('POST_CREATED', {postCreated: newPost});
→ subscribe 중인 모든 client 에 pushSubscription 은 보통 WebSocket 위. graphql-ws 표준 protocol. SSE 도 가능.
Schema 의 강력함 — introspection
# Special query
{
__schema {
types {
name
fields {
name
type { name }
}
}
}
}
→ 전체 schema 의 metadata 반환효과:
- 자동 docs — GraphiQL / GraphQL Playground 가 schema 보고 docs 생성
- 타입 안전 client — GraphQL Code Generator 가 TypeScript 타입 자동 생성
- tooling 풍부 — Apollo / Relay / urql 의 cache /optimization
Trade-off — production 에서 introspection 비활성화 권장. attacker 가 schema 보고 internal 구조 파악 막음.
Federation — 분산 GraphQL
Microservice 환경에서 여러 GraphQL service 를 한 endpoint 로:
Service A (users):
type User @key(fields: "id") {
id: ID!
name: String!
}
Service B (posts):
type Post {
id: ID!
title: String!
author: User! ← User 정의는 Service A 에 있음
}
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post!]! ← Service B 가 User 에 posts field 추가
}
Apollo Gateway / Mesh:
- 두 service 의 schema 결합
- query 분석 후 적절한 service 로 라우팅
- 결과 합쳐서 client 에 응답대형 조직 (Netflix, Walmart) 의 표준. 작은 팀은 monolithic GraphQL 충분.
REST vs GraphQL — 매트릭스
| REST | GraphQL | |
|---|---|---|
| endpoint | resource 마다 (수십~수백) | 1 개 (/graphql) |
| over-fetching | 흔함 | 없음 (정확히 요청한 것만) |
| under-fetching | 흔함 (여러 호출 필요) | 없음 (한 query 에) |
| versioning | /v1, /v2 | schema 진화 + @deprecated |
| caching | HTTP cache 자동 (GET URL 기반) | POST 라 어려움. Apollo 의 client cache 별도 |
| tooling | OpenAPI / Postman 등 | GraphiQL / Apollo Studio |
| learning curve | HTTP 만 알면 | schema 설계 + resolver + N+1 등 필요 |
| file upload | 자연 (multipart) | graphql-upload spec 별도 |
| tooling 성숙 | 30 년 | 10 년 |
언제 GraphQL X — over-engineering
- 단순 CRUD app — REST 가 더 빠르고 익숙
- public API — caching / CDN 활용에 REST 가 유리
- 파일 upload / streaming 위주 — multipart REST 가 자연
- 작은 팀 + 단일 client — schema 설계 부담만
GraphQL 적합:
- 모바일 + web client 둘 다 — 화면마다 다른 데이터 모양
- 관계 데이터 깊은 nested fetch — REST 의 round-trip 부담
- 여러 micro service 의 unified API — federation
- BFF (Backend for Frontend) — client 의 필요에 맞춰 정확히 응답
흔한 함정
1. N+1 무시
DataLoader 안 쓰면 production 에서 DB 폭주. 모든 nested resolver 에 DataLoader 권장.
2. complex query 의 DoS
# 공격자의 query
{
users {
posts {
comments {
author {
friends {
posts {
comments {
...
}
}
}
}
}
}
}
}
→ 백만 row fetch 가능. server down.
방어:
- query depth limit (graphql-depth-limit)
- query complexity (graphql-query-complexity)
- persisted queries (pre-approved query 만 허용)3. circular fragment
fragment A on User { friends { ...A } }
↑ 자기 자신 참조
→ 무한 loop. spec 이 막지만 일부 구현 누락.4. 모든 field 가 nullable
Schema 에 ! 안 박으면 모두 nullable. client 가 매번 null check. name: String! 명시.
5. Error handling 의 모호함
# REST
GET /users/999 → 404 Not Found
# GraphQL
{
"data": {"user": null},
"errors": [{"message": "User not found"}]
}
→ HTTP 200 OK. errors array 가 응답에 박힘. client 가 errors 도 체크.참고 자료
- GraphQL specification — spec.graphql.org
- DataLoader (Facebook) — GitHub
- Apollo Federation — apollographql.com
- Why we built GraphQL (Facebook 2015) — Meta Engineering
요약
- GraphQL = client 가 정확히 원하는 모양 요청. 1 endpoint, 1 query, 정확한 응답.
- Schema-first — SDL 로 타입과 query 정의. Client/server 의 contract.
- Resolver = field 별 함수. lazy 실행 — 요청된 field 만.
- N+1 문제는 GraphQL 의 가장 큰 함정 — DataLoader 가 batching + memoization 으로 해결.
- Mutation = write 작업 (sequential). Subscription = WebSocket 위 실시간 push.
- Introspection 으로 자동 docs / type-safe client / tooling. production 비활성화 권장.
- Federation = 여러 GraphQL service 의 unified gateway. microservice 환경 표준.
- REST vs GraphQL — 화면마다 다른 데이터 / 깊은 nested / 여러 client 면 GraphQL. 단순 CRUD / public API / file upload 는 REST.