GraphQL vs REST API 실전 비교: 2년간 운영 경험에서 배운 것들

최근 2년간 다양한 프로젝트에서 GraphQL과 REST API를 모두 운영해본 경험을 바탕으로 두 기술을 비교합니다. API 설계 선택은 단순히 기술 트렌드가 아니라 프로젝트 특성과 팀 환경에 맞춰 결정해야 합니다.

우리 팀의 상황

2022년, 새로운 프로젝트를 시작하면서 API 설계 방식을 결정해야 했습니다:

  • 프로젝트 A: 복잡한 대시보드 앱, 다양한 데이터 조합 필요
  • 프로젝트 B: 단순 CRUD 중심 관리자 페이지
  • 프로젝트 C: 모바일 앱 백엔드, 대역폭 최적화 중요

결론적으로 A와 C는 GraphQL, B는 REST를 선택했습니다. 그 이유를 설명합니다.

GraphQL의 장점

1. Over-fetching/Under-fetching 해결

REST에서 흔히 겪는 문제:

JS
// REST: 사용자 정보 + 주문 내역이 필요할 때
GET /api/users/123           // 불필요한 필드까지 모두 받음
GET /api/users/123/orders    // 추가 요청 필요

// GraphQL: 필요한 것만 한 번에
query {
  user(id: "123") {
    name
    email
    orders(limit: 5) {
      id
      totalAmount
      status
    }
  }
}

모바일 앱(프로젝트 C)에서는 이 차이가 데이터 전송량 40% 감소로 이어졌습니다.

2. 강력한 타입 시스템

GRAPHQL
# GraphQL Schema
type User {
  id: ID!
  name: String!
  email: String!
  role: UserRole!
  orders: [Order!]!
  createdAt: DateTime!
}

enum UserRole {
  ADMIN
  USER
  GUEST
}

type Query {
  user(id: ID!): User
  users(filter: UserFilter, pagination: PaginationInput): UserConnection!
}

스키마 자체가 문서이자 계약입니다. TypeScript와 결합하면 프론트엔드-백엔드 타입 일관성을 보장할 수 있습니다.

3. 자체 문서화

GraphQL Playground나 Apollo Studio에서 스키마를 바로 탐색할 수 있습니다. Swagger 같은 별도 문서 도구 없이도 API 명세를 확인할 수 있어 문서 유지보수 비용이 제로에 가깝습니다.

4. 실시간 구독 지원

GRAPHQL
// 채팅 메시지 실시간 구독
subscription OnNewMessage($roomId: ID!) {
  messageAdded(roomId: $roomId) {
    id
    content
    sender {
      name
      avatar
    }
    createdAt
  }
}

WebSocket 기반 Subscription으로 실시간 기능을 쉽게 구현할 수 있습니다.

REST API의 장점

1. 단순성과 직관성

BASH
GET    /api/products          # 목록 조회
GET    /api/products/123      # 단일 조회
POST   /api/products          # 생성
PUT    /api/products/123      # 수정
DELETE /api/products/123      # 삭제

엔드포인트 구조가 명확하고, HTTP 메서드만으로 의도를 파악할 수 있습니다. 주니어 개발자도 바로 이해할 수 있는 직관성이 있습니다.

2. HTTP 캐싱 활용

BASH
# REST: HTTP 캐시 헤더 활용
GET /api/products/123
Cache-Control: max-age=3600
ETag: "abc123"

# GraphQL: POST 요청이라 HTTP 캐싱 불가
POST /graphql
{body: {query: "..."}}  # 매번 서버 처리 필요

REST는 CDN 캐싱을 쉽게 적용할 수 있어, 읽기 중심 API에서 큰 성능 이점이 있습니다.

3. 파일 업로드 간편

JS
// REST: 표준 multipart/form-data
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'Profile photo');
await fetch('/api/upload', { method: 'POST', body: formData });

// GraphQL: 별도 스펙(graphql-upload) 필요
// 설정이 복잡하고 일부 환경에서 호환성 이슈

4. 디버깅 도구 풍부

Postman, Insomnia, curl 등 성숙한 도구 생태계가 있습니다. 문제 발생 시 재현과 디버깅이 용이합니다.

실제 운영에서 겪은 이슈

GraphQL의 함정: N+1 쿼리

JS
// 이 쿼리가 실행되면...
query {
  users {
    name
    orders {
      id
    }
  }
}

// 내부적으로 이렇게 됨 (N+1 문제)
SELECT * FROM users;
SELECT * FROM orders WHERE user_id = 1;
SELECT * FROM orders WHERE user_id = 2;
SELECT * FROM orders WHERE user_id = 3;
... (사용자 수만큼 반복)

해결책: DataLoader 패턴

JS
const orderLoader = new DataLoader(async (userIds) => {
  const orders = await Order.findAll({
    where: { userId: userIds }
  });
  // userIds 순서에 맞게 그룹핑하여 반환
  return userIds.map(id => orders.filter(o => o.userId === id));
});

REST의 함정: API 버전 관리

기능 추가 시 하위 호환성 유지가 어렵습니다:

BASH
/api/v1/users  # 기존
/api/v2/users  # 새 필드 추가
/api/v3/users  # 구조 변경

# 결국 여러 버전을 동시에 유지해야 함

GraphQL은 스키마 확장이 자연스러워 버전 없는 API가 가능합니다.

선택 가이드라인

상황추천
복잡한 데이터 관계, 다양한 클라이언트GraphQL
단순 CRUD, 빠른 개발 필요REST
모바일 앱 (대역폭 중요)GraphQL
퍼블릭 API, 외부 개발자 대상REST
실시간 기능 필요GraphQL
레거시 시스템 연동REST

결론

"GraphQL이 더 좋다" 또는 "REST가 정답"이라는 이분법은 틀렸습니다.

프로젝트의 복잡도, 팀의 경험, 클라이언트 요구사항을 고려하여 선택해야 합니다. 때로는 같은 서비스 내에서 두 가지를 함께 사용하는 것도 좋은 전략입니다.