새 프로젝트를 시작할 때 API 설계 방식으로 REST와 GraphQL 중 무엇을 선택할지 고민하는 경우가 많습니다. 두 방식 모두 훌륭하지만, 상황에 따라 적합한 선택이 다릅니다. 각각의 특징을 이해하고 올바른 선택을 해봅시다.

REST API란?

REST(Representational State Transfer)는 URL로 리소스를 나타내고 HTTP 메서드(GET, POST, PUT, DELETE)로 동작을 표현하는 아키텍처 스타일입니다.

# 사용자 목록 조회
GET /api/users

# 특정 사용자 조회
GET /api/users/1

# 사용자 생성
POST /api/users
Content-Type: application/json
{ "name": "홍길동", "email": "hong@example.com" }

# 사용자 수정
PUT /api/users/1
{ "name": "홍길순" }

# 사용자 삭제
DELETE /api/users/1

GraphQL이란?

GraphQL은 Facebook이 개발한 쿼리 언어입니다. 클라이언트가 필요한 데이터를 정확히 명시해서 요청할 수 있습니다. 단 하나의 엔드포인트(/graphql)로 모든 요청을 처리합니다.

# 필요한 필드만 정확히 요청
query GetUser {
  user(id: 1) {
    name
    email
    posts {
      title
      createdAt
    }
  }
}

# 뮤테이션 (데이터 변경)
mutation CreateUser {
  createUser(input: { name: "홍길동", email: "hong@example.com" }) {
    id
    name
  }
}

핵심 차이점 비교

항목RESTGraphQL
엔드포인트리소스마다 별도 URL단일 엔드포인트
데이터 요청서버가 응답 형태 결정클라이언트가 필요한 필드 지정
Over-fetching발생 가능없음
Under-fetching발생 가능 (N+1 문제)한 번에 해결
캐싱HTTP 캐싱 기본 지원별도 설정 필요
학습 곡선낮음높음
파일 업로드쉬움추가 설정 필요

REST가 적합한 경우

  • 간단한 CRUD 서비스
  • 퍼블릭 API (외부 개발자가 사용하는 경우)
  • HTTP 캐싱이 중요한 경우
  • 팀이 REST에 익숙한 경우
  • 파일 업로드가 핵심 기능인 경우

GraphQL이 적합한 경우

  • 모바일 앱처럼 네트워크 효율이 중요한 클라이언트
  • 다양한 클라이언트(웹, 앱, 서드파티)가 동일 API를 사용하는 경우
  • 복잡하게 연결된 데이터 구조 (소셜 네트워크, 추천 시스템)
  • 프론트엔드 팀이 API 스펙을 자율적으로 관리해야 하는 경우

실전 예시: 블로그 API

블로그 게시글 목록 페이지에서 작성자 정보도 함께 보여야 한다면:

// REST — 2번의 요청 필요 (N+1 문제)
const posts = await fetch('/api/posts').then(r => r.json());
const authors = await Promise.all(
  posts.map(p => fetch(`/api/users/${p.authorId}`).then(r => r.json()))
);

// GraphQL — 1번의 요청으로 해결
const { data } = await fetch('/graphql', {
  method: 'POST',
  body: JSON.stringify({
    query: `{
      posts {
        title
        author { name avatar }
        createdAt
      }
    }`
  })
});
결론
REST와 GraphQL은 경쟁 관계가 아닙니다. 단순한 서비스는 REST로 빠르게 시작하고, 클라이언트가 다양하거나 데이터 관계가 복잡해질수록 GraphQL 전환을 고려하세요. 혼합 사용도 가능합니다.

REST API 버전 관리 전략

API를 외부에 공개했다면 기존 클라이언트를 깨뜨리지 않으면서 변경해야 합니다. 버전 관리 방법은 크게 세 가지입니다.

# 1. URL 경로에 버전 포함 (가장 일반적)
GET /api/v1/users
GET /api/v2/users

# 2. 쿼리 파라미터
GET /api/users?version=2

# 3. Accept 헤더 (REST 원칙에 가장 부합)
Accept: application/vnd.myapp.v2+json

URL 경로 버전 관리(/v1/, /v2/)가 가장 직관적이고 캐싱·로깅에도 유리합니다. 팀 규모와 API 복잡도에 따라 적합한 전략을 선택하세요.

GraphQL Fragment와 Variables

GraphQL의 Fragment로 반복되는 필드를 재사용하고, Variables로 안전하게 동적 값을 전달할 수 있습니다.

fragment UserFields on User {
  id
  name
  email
  avatar
}

query GetPostWithAuthor($postId: ID!) {
  post(id: $postId) {
    title
    content
    author {
      ...UserFields
    }
    comments {
      text
      author {
        ...UserFields
      }
    }
  }
}

Variables를 사용하면 쿼리 문자열에 값을 직접 삽입하지 않으므로 인젝션 공격을 방지하고 쿼리 캐싱 효율도 높아집니다.

GraphQL N+1 문제와 DataLoader

GraphQL의 대표적인 성능 함정인 N+1 문제는, 목록을 조회할 때 각 항목의 연관 데이터를 개별 쿼리로 가져오는 현상입니다.

// N+1 문제 — posts 10개면 DB 쿼리 11번 발생
const resolvers = {
  Query: {
    posts: () => db.posts.findAll(),         // 쿼리 1번
  },
  Post: {
    author: (post) => db.users.findById(post.authorId), // 포스트 수만큼 반복!
  },
};

// DataLoader로 해결 — 배치 처리
import DataLoader from 'dataloader';

const userLoader = new DataLoader(async (userIds) => {
  const users = await db.users.findAll({ where: { id: userIds } });
  return userIds.map(id => users.find(u => u.id === id));
});

Post: {
  author: (post) => userLoader.load(post.authorId), // 자동 배치 처리
}

REST API 보안 필수 체크리스트

  • 인증 — JWT Bearer 토큰 또는 API Key. 토큰은 Authorization: Bearer <token> 헤더로 전달
  • HTTPS 강제 — HTTP 요청을 HTTPS로 리다이렉트, HSTS 헤더 설정
  • Rate Limiting — IP 또는 사용자별 요청 횟수 제한
  • 입력 검증 — 모든 요청 파라미터를 서버에서 검증. 클라이언트 검증만으로는 부족
  • CORS 명시Access-Control-Allow-Origin: *는 공개 API가 아니면 사용 금지

실제 서비스에서의 API 설계 사례

이론적인 API 설계 원칙을 실제 서비스에 적용할 때는 다양한 현실적 제약이 따릅니다. 국내 주요 서비스들이 어떻게 API를 설계하고 발전시켜 왔는지 살펴보면 좋은 교훈을 얻을 수 있습니다.

대부분의 스타트업은 초기에 REST API로 시작합니다. 처음에는 단순한 CRUD 기능만 있고 클라이언트도 웹 하나뿐이라 REST가 충분히 효과적입니다. 그러나 서비스가 성장하면서 모바일 앱이 추가되고, 파트너사에 데이터를 제공해야 하며, 각 클라이언트마다 필요한 데이터의 형태가 달라지기 시작합니다. 바로 이 시점에서 GraphQL 도입을 검토하는 팀이 많습니다.

카카오, 라인, 쿠팡 같은 국내 주요 기업들도 내부적으로 REST와 GraphQL을 혼용하는 경우가 많습니다. 오래된 서비스의 핵심 API는 REST로 유지하면서, 새로운 기능이나 BFF(Backend For Frontend) 레이어에는 GraphQL을 도입하는 전략입니다. 이런 접근은 기존 시스템을 안정적으로 유지하면서 새 기술의 이점도 누릴 수 있어 실용적입니다.

API 설계에서 또 하나 중요한 것은 문서화입니다. REST API는 OpenAPI(Swagger) 사양으로, GraphQL은 내장된 스키마 시스템으로 자동 문서화가 가능합니다. 문서가 없는 API는 협업을 어렵게 만들고, 잘못된 사용을 유도할 수 있습니다. Swagger UI나 GraphQL Playground를 개발 환경에 제공하면 프론트엔드 개발자와의 협업 효율이 크게 높아집니다.

API 버전 관리와 하위 호환성 유지하기

서비스가 성장하면서 API를 변경해야 할 상황이 반드시 옵니다. 기존 클라이언트를 깨뜨리지 않으면서 API를 발전시키는 방법이 버전 관리입니다. 가장 간단한 방식은 URL 경로에 버전을 포함하는 것입니다. /api/v1/users/api/v2/users를 동시에 운영하면서 새 버전을 점진적으로 마이그레이션하는 방식입니다. 이 방식은 이해하기 쉽고 캐싱도 버전별로 명확히 구분됩니다. 하지만 여러 버전을 병렬로 유지해야 해 운영 비용이 높아진다는 단점이 있습니다.

GraphQL은 타입 시스템 덕분에 URL 버전 없이도 하위 호환성을 유지하며 발전할 수 있습니다. 기존 필드를 deprecated 처리하고 새 필드를 추가하는 방식으로 점진적 마이그레이션이 가능합니다. 그러나 필드가 누적되면 스키마가 지저분해지고, deprecated 필드를 언제 제거할지 결정하기 어려운 문제가 생깁니다. 어떤 전략을 쓰든 중요한 것은 변경 사항을 클라이언트 팀에 충분한 사전 공지와 함께 전달하고, 마이그레이션 기간 동안 구버전과 신버전을 함께 지원하는 것입니다. Sunset 헤더를 사용해 특정 API 버전의 지원 종료 일자를 클라이언트에게 알리는 방식을 표준화하면 마이그레이션 커뮤니케이션이 훨씬 체계적으로 관리됩니다.