API 설계의 갈림길에서
새 프로젝트를 시작할 때 API 설계 방식을 결정하는 것은 아키텍처의 근간을 정하는 일입니다. 한번 선택하면 변경 비용이 크기 때문에, 각 접근법의 특성을 정확히 이해하고 프로젝트의 요구사항에 맞는 선택을 해야 합니다.
2024 Postman API Report에 따르면, 기업의 89%가 REST를 사용하고 있고, GraphQL은 35%의 채택률을 보입니다(중복 응답). GraphQL이 빠르게 성장하고 있지만, REST가 여전히 지배적인 것은 분명합니다. 그렇다면 어떤 상황에서 어떤 것을 선택해야 할까요?
---
핵심 차이점 — 데이터 요청 방식
REST — 서버가 응답 구조를 결정
``` GET /api/users/123 → { id: 123, name: "김개발", email: "[email protected]", bio: "...", avatar: "...", createdAt: "...", settings: {...}, preferences: {...} }
GET /api/users/123/posts → [{ id: 1, title: "...", body: "...(전체 본문)...", comments: [{...}, {...}], tags: [...] }] ```
REST에서는 각 엔드포인트가 고정된 데이터 구조를 반환합니다. 사용자 이름만 필요해도 모든 필드가 반환되고(Over-fetching), 게시글과 댓글을 함께 보려면 여러 엔드포인트를 호출해야 합니다(Under-fetching).
GraphQL — 클라이언트가 응답 구조를 결정
```graphql query { user(id: "123") { name posts(first: 5) { title commentCount tags { name } } } } ```
GraphQL은 클라이언트가 필요한 필드만 정확히 요청합니다. 한 번의 요청으로 여러 리소스를 가져올 수 있고, 불필요한 데이터 전송이 없습니다. 모바일 환경처럼 대역폭이 제한된 상황에서 특히 유리하죠.
---
타입 시스템과 개발자 경험
GraphQL의 스키마
```graphql type User { id: ID! name: String! email: String! posts(first: Int, after: String): PostConnection! }
type Post { id: ID! title: String! body: String! author: User! tags: [Tag!]! createdAt: DateTime! }
type Query { user(id: ID!): User posts(filter: PostFilter): PostConnection! }
type Mutation { createPost(input: CreatePostInput!): Post! updatePost(id: ID!, input: UpdatePostInput!): Post! } ```
GraphQL 스키마는 API의 자기 문서화(self-documenting) 역할을 합니다. 스키마만 보면 어떤 데이터를 요청할 수 있는지, 각 필드의 타입은 무엇인지, 어떤 인자가 필요한지 명확히 알 수 있습니다. GraphQL Playground나 Apollo Studio 같은 도구를 통해 스키마를 탐색하고 쿼리를 테스트할 수 있어, Swagger/OpenAPI 같은 별도 문서화 도구가 불필요합니다.
REST의 OpenAPI
REST에서도 OpenAPI(Swagger) 스펙으로 API를 문서화할 수 있지만, 스펙과 실제 구현이 불일치하는 문제가 빈번합니다. 코드를 먼저 작성하고 문서를 나중에 작성하는 팀에서 특히 그렇죠. 반면 GraphQL은 스키마가 곧 런타임이므로 문서와 구현의 불일치가 구조적으로 불가능합니다.
---
캐싱 — REST의 강점
REST API는 HTTP 캐싱 인프라를 자연스럽게 활용합니다. 각 URL이 고유한 리소스를 나타내므로, CDN, 브라우저 캐시, 프록시 캐시가 별도 설정 없이 작동합니다.
``` GET /api/posts/456 Cache-Control: public, max-age=3600 ETag: "abc123" ```
GraphQL은 모든 요청이 단일 엔드포인트(`POST /graphql`)로 전송되고, 요청 본문에 따라 응답이 달라지므로 HTTP 캐싱이 직접 적용되지 않습니다. Apollo Client의 정규화된 캐시나 Persisted Queries 같은 별도 솔루션이 필요합니다.
이것이 REST의 명확한 강점입니다. 캐싱이 성능의 핵심인 공개 API나 콘텐츠 서비스에서는 REST가 유리할 수 있습니다.
---
N+1 문제 — GraphQL의 아킬레스건
GraphQL의 유연한 쿼리는 양날의 검이 될 수 있습니다. 클라이언트가 중첩된 관계를 요청하면, 서버에서 N+1 쿼리 문제가 발생할 수 있습니다.
```graphql # 이 쿼리는 내부적으로 1 + N개의 DB 쿼리를 유발할 수 있음 query { posts(first: 20) { # 1 쿼리 title author { # 20개 쿼리 (각 post의 author) name } } } ```
DataLoader는 이 문제의 표준 해결책입니다. 같은 이벤트 루프 틱에서 발생하는 동일한 타입의 요청을 배치(batch)로 모아서 한 번의 쿼리로 처리합니다.
```typescript import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (userIds: string[]) => { const users = await db.user.findMany({ where: { id: { in: userIds } }, }); // userIds 순서에 맞게 정렬하여 반환 const userMap = new Map(users.map(u => [u.id, u])); return userIds.map(id => userMap.get(id) ?? null); }); ```
---
실시간 데이터 — GraphQL Subscriptions
GraphQL은 Subscription을 통해 실시간 데이터 스트리밍을 스키마 수준에서 지원합니다.
```graphql type Subscription { messageAdded(chatId: ID!): Message! notificationReceived: Notification! } ```
REST에서도 WebSocket이나 Server-Sent Events(SSE)로 실시간 기능을 구현할 수 있지만, GraphQL은 실시간 데이터를 쿼리와 동일한 타입 시스템 안에서 관리할 수 있다는 점이 다릅니다.
---
어떤 것을 선택할까 — 판단 기준
REST가 더 적합한 경우: 단순한 CRUD API, 캐싱이 중요한 공개 API, 파일 업로드/다운로드 중심 서비스, 팀이 REST에 익숙하고 GraphQL 경험이 없는 경우, 마이크로서비스 간 내부 통신.
GraphQL이 더 적합한 경우: 복잡한 관계형 데이터 모델, 다양한 클라이언트(웹, iOS, Android)에 서로 다른 데이터 제공, 프론트엔드와 백엔드 팀이 독립적으로 개발, 실시간 기능이 핵심인 서비스, 빠른 프로토타이핑과 반복이 중요한 경우.
하이브리드 접근: 실무에서는 두 가지를 혼용하는 경우도 많습니다. 메인 API는 GraphQL로 구축하되, 파일 업로드나 webhook 같은 특정 기능은 REST 엔드포인트를 별도로 제공하는 식입니다.
GraphQL과 REST는 "어느 것이 더 좋은가"의 문제가 아니라 "어떤 상황에 더 적합한가"의 문제입니다. REST의 단순함과 캐싱 친화성, GraphQL의 유연성과 타입 안전성 — 프로젝트의 요구사항과 팀의 역량을 고려하여 신중하게 선택하시기 바랍니다.