03 — GraphQL vs REST
한 줄 답: REST는 리소스가 고정이고 GraphQL은 응답 모양이 가변이다. 둘은 같은 자리에서 다른 문제를 푼다 — 대체재가 아니라 다른 도구다.
Why — 왜 이 비교가 필요한가
“REST를 GraphQL로 마이그레이션하자”는 말이 회의실에서 나올 때, 보통 셋 중 하나가 빠져 있다 — 왜 마이그레이션하는가, 무엇을 잃는가, 얻는 것이 그 손실을 정당화하는가.
REST는 죽지 않았고, GraphQL은 만능이 아니다. 둘은 다른 트레이드오프를 가진다. 이 문서는 그 트레이드오프를 표 한 장과 결정 트리 하나로 분리한다.
How — 어떻게 다른가
1) 핵심 차이는 응답 모양을 누가 결정하나
REST에서 클라이언트는 어떤 리소스를 받을지 선택할 뿐, 그 리소스 안에 무엇이 들어있을지는 서버가 정한다. GraphQL에서 클라이언트는 어떤 필드를 원하는지까지 선언한다.
2) Over-fetching / Under-fetching
Over-fetching — 필요 없는 필드까지 받는 것
GET /users/123
→ {
"id": 123,
"name": "Alice",
"email": "...", ← 필요 없음
"phoneNumber": "...", ← 필요 없음
"address": { ... }, ← 필요 없음
"preferences": { ... } ← 필요 없음
}Under-fetching — 필요한 데이터를 받으려면 N번 호출해야 하는 것
GET /users/123 → user
GET /users/123/posts → posts
GET /posts/1/comments → comments for post 1
GET /posts/2/comments → comments for post 2
GET /posts/3/comments → comments for post 3
... (N+1)GraphQL은 둘 다 한 번에 푼다.
{
user(id: 123) {
name
posts {
title
comments { text }
}
}
}→ 응답 한 번. 필요한 필드만.
3) Endpoint 구조
| 측면 | REST | GraphQL |
|---|---|---|
| URL 패턴 | /resource/:id/sub 다수 | /graphql 하나 |
| HTTP 메서드 | GET/POST/PUT/PATCH/DELETE | 거의 항상 POST |
| 식별 | URL path | body의 operation name |
| 캐시 키 | URL | (없음 — body 기반) |
| Status code | 200/4xx/5xx 의미 풍부 | 거의 항상 200 (errors 배열 사용) |
4) 타입 시스템
REST는 타입이 없다 — OpenAPI/Swagger를 따로 붙여야 타입을 얻는다. GraphQL은 spec 자체에 타입 시스템이 박혀 있다. 모든 필드의 타입을 런타임에 introspection으로 조회 가능하다.
# 스키마 자체가 계약(contract)
type User {
id: ID!
name: String!
email: String
posts: [Post!]!
}REST + OpenAPI도 같은 일을 하지만, OpenAPI는 외부 도구고 GraphQL 타입 시스템은 spec이다 — 없으면 GraphQL이 아니다.
What — 구체 비교
비교표 — 한 장으로 끝
| 영역 | REST | GraphQL |
|---|---|---|
| 응답 모양 결정자 | 서버 | 클라이언트 |
| Over-fetching | 흔함 | 없음 (선언한 것만) |
| Under-fetching | 흔함 (N+1) | 없음 (한 번에) |
| Endpoint 수 | 많음 (/users, /posts, …) | 1개 (/graphql) |
| 타입 시스템 | 외부(OpenAPI) | spec에 내장 |
| HTTP 캐싱 | 강함 (URL + ETag) | 약함 (body 기반, 클라이언트 캐시 필요) |
| CDN 캐싱 | 쉬움 | 어려움 (persisted query로 완화) |
| Status code | 의미 풍부 (4xx/5xx 활용) | 거의 200 (errors 배열) |
| 파일 업로드 | 자연스러움 (multipart) | 별도 spec 필요 (graphql-multipart-request) |
| 학습 곡선 | 낮음 | 중간 (스키마·resolver·N+1) |
| 도구 생태계 | Postman·OpenAPI·Insomnia | GraphiQL·Apollo Studio·Hasura |
| Rate limit | path 기반 쉬움 | complexity·depth 분석 필요 |
| 관측 (observability) | URL별로 자연스러움 | operation name 기반 |
| 버전 관리 | URL versioning (/v2/) | 필드 단위 deprecation |
| Mobile / 저대역 환경 | 약함 (round trip 많음) | 강함 (한 번에) |
| 단순 CRUD | 강함 | 과잉 |
| 가변 view (대시보드) | 약함 | 강함 |
| Real-time | SSE/WebSocket 별도 | subscription operation 내장 |
REST가 잘 맞는 곳
| 상황 | 이유 |
|---|---|
| 단순 CRUD 리소스 | 모양이 고정 — over-fetching이 작다 |
| 공개 API | HTTP 캐시·CDN·문서화 도구가 성숙 |
| 파일 업로드/다운로드 | multipart·range request가 자연스러움 |
| Webhook | 외부에서 호출하기 좋음 |
| 인프라 친화 (rate limit, WAF) | path 기반 정책이 그대로 동작 |
GraphQL이 잘 맞는 곳
| 상황 | 이유 |
|---|---|
| 가변 view를 그리는 UI | 화면마다 필요한 필드가 다름 |
| Mobile / 저대역 환경 | 한 번에 끝나는 round trip이 가치 큼 |
| 마이크로서비스 합성 (BFF) | Federation으로 여러 서비스를 한 endpoint로 |
| Real-time + query 혼합 | subscription을 같은 스키마에서 |
| 클라이언트가 빠르게 진화 | 서버 수정 없이 새 필드 조합 가능 |
결정 트리
→ 보통 공개 API는 REST, 사내 모바일·웹은 GraphQL, 외부 도메인 통합은 둘 다가 정답이다.
What-if — 잘못 쓰면
1) “REST를 GraphQL로 그대로 옮기면” 발생하는 일
증상: REST의 한 endpoint = GraphQL의 한 쿼리로 1:1 매핑하면, GraphQL의 모든 이점이 사라지고 단점만 남는다. 원인: GraphQL의 가치는 그래프 탐색인데 REST 매핑은 단일 리소스 호출에 머문다. 대응: 매핑이 아니라 재설계. 화면 단위로 어떤 데이터를 원하는지부터 그린다.
2) HTTP 캐시가 깨진다
증상: REST에서 잘 동작하던 CDN·proxy 캐시가 GraphQL에선 무용지물.
원인: 모든 요청이 POST /graphql. URL이 동일해 캐시 키가 안 잡힘.
대응: Persisted Queries (쿼리를 hash로 미리 등록 → URL에 hash 포함) 또는 Automatic Persisted Queries(APQ).
3) Rate limiting이 무력화된다
증상: 악의적 클라이언트가 단일 쿼리로 서버 전체 데이터를 요청.
원인: REST의 path 기반 rate limit이 GraphQL의 body 기반과 안 맞는다.
대응: query complexity analysis + depth limiting + operation-level rate limit. (이 KB의 07-security-governance 챕터에서 자세히.)
4) “200 OK인데 데이터가 부분만 왔다”
증상: 클라이언트가 success로 판단하고 data.user.name을 읽으려는데 null.
원인: GraphQL spec은 partial response를 허용. 일부 resolver가 실패해도 200 OK + errors 배열.
대응: 클라이언트는 errors 배열을 항상 검사. Apollo Client·urql 등은 기본으로 처리.
5) Mobile 트래픽이 늘어났다
증상: GraphQL 도입했는데 모바일 데이터 사용량이 증가. 원인: 쿼리 텍스트가 매 요청에 포함됨. 짧은 쿼리도 1~2KB. 대응: Persisted Queries로 쿼리를 hash로 치환. 요청 본문이 수십 바이트로 줄어듦.
6) N+1을 모른 채 production에 올렸다
증상: 한 쿼리당 DB 쿼리 수백 개 발생, p99 응답 폭주.
원인: GraphQL resolver는 필드마다 호출된다. posts { author { name } }에서 author resolver가 post 개수만큼 호출.
대응: DataLoader 패턴으로 배치·캐시. (이 KB의 03-n-plus-1-dataloader 챕터에서.)
Insight — 흥미로운 이야기
”REST 만든 사람이 GraphQL을 어떻게 봤나”
Roy Fielding(REST 박사논문 저자)은 GraphQL을 RPC의 변종으로 본다 — hypermedia constraints(HATEOAS)를 따르지 않으므로 진정한 REST가 아니라는 입장. 학술적으로는 맞는 말이지만, 실무에서 HATEOAS를 진짜로 따르는 REST는 거의 없다 — 우리가 “REST”라고 부르는 것의 대부분은 RPC over HTTP with JSON이다.
이 관점에서 보면 GraphQL은 더 정직한 RPC다 — 자기가 RPC라는 사실을 숨기지 않는다.
”GitHub의 두 API”
GitHub은 REST v3와 GraphQL v4를 동시에 운영한다. 외부 개발자들이 “왜 둘 다 있느냐”고 물을 때마다 답은 같다 — 공개 API는 REST가 적합하고, 내부 도구·복합 query는 GraphQL이 적합. 한쪽이 다른 쪽을 대체하지 않는다.
비슷한 예: Shopify는 REST를 deprecated로 표시하고 GraphQL을 밀고 있지만, 결제·webhook 같은 단순 리소스는 여전히 REST가 더 자연스럽다는 평가.
”BFF 패턴 — 둘을 같이 쓰는 법”
대형 조직의 가장 흔한 결론은 둘 다 쓴다다.
[Mobile / Web] --(GraphQL)--> [BFF GraphQL Layer]
|
┌───────────┼───────────┐
↓ ↓ ↓
REST 마이크로 gRPC legacy SOAP내부 마이크로서비스는 REST/gRPC로 단순하게 두고, 클라이언트 면은 GraphQL BFF로 합성한다. 이 패턴이 Netflix·Airbnb·GitHub의 현재 표준이다.
요약 + 다이어그램
REST와 GraphQL은 다른 자리에 있다. REST는 리소스가 고정인 곳에서 강하고, GraphQL은 응답 모양이 가변인 곳에서 강하다. 마이그레이션의 정답은 대체가 아니라 공존 — BFF 패턴이 산업 표준이다.
다음 문서:
04-anatomy-of-a-query.mdx— 그렇다면 GraphQL 쿼리 한 개는 어떻게 생겼나?