🔷 GraphQL0. GraphQL의 기초03 — GraphQL vs REST

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 구조

측면RESTGraphQL
URL 패턴/resource/:id/sub 다수/graphql 하나
HTTP 메서드GET/POST/PUT/PATCH/DELETE거의 항상 POST
식별URL pathbody의 operation name
캐시 키URL(없음 — body 기반)
Status code200/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 — 구체 비교

비교표 — 한 장으로 끝

영역RESTGraphQL
응답 모양 결정자서버클라이언트
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·InsomniaGraphiQL·Apollo Studio·Hasura
Rate limitpath 기반 쉬움complexity·depth 분석 필요
관측 (observability)URL별로 자연스러움operation name 기반
버전 관리URL versioning (/v2/)필드 단위 deprecation
Mobile / 저대역 환경약함 (round trip 많음)강함 (한 번에)
단순 CRUD강함과잉
가변 view (대시보드)약함강함
Real-timeSSE/WebSocket 별도subscription operation 내장

REST가 잘 맞는 곳

상황이유
단순 CRUD 리소스모양이 고정 — over-fetching이 작다
공개 APIHTTP 캐시·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 v3GraphQL 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 쿼리 한 개는 어떻게 생겼나?