네트워크Medium#94
REST API와 GraphQL의 차이점을 설명하고, 각각 어떤 상황에 적합한지 설명해주세요.
#네트워크#REST#GraphQL#API#설계
힌트
오버패칭(Over-fetching)과 언더패칭(Under-fetching)의 개념을 생각해보세요.
정답 및 해설
REST API와 GraphQL의 차이점을 설명하고, 각각 어떤 상황에 적합한지 설명해주세요.
REST API와 GraphQL은 모두 클라이언트-서버 간 데이터 통신을 위한 아키텍처/쿼리 언어입니다. REST는 리소스 기반의 고정된 엔드포인트를 사용하고, GraphQL은 단일 엔드포인트에서 클라이언트가 필요한 데이터를 직접 지정하는 방식을 사용합니다. 각각 장단점이 있어 상황에 맞게 선택해야 합니다.
REST API
기본 개념
REST(Representational State Transfer)는 **리소스(Resource)**를 URL로 표현하고, HTTP 메서드로 작업을 나타내는 아키텍처 스타일입니다.
리소스와 엔드포인트:
GET /users → 사용자 목록
GET /users/:id → 특정 사용자 조회
POST /users → 사용자 생성
PUT /users/:id → 사용자 전체 수정
PATCH /users/:id → 사용자 일부 수정
DELETE /users/:id → 사용자 삭제
GET /users/:id/posts → 특정 사용자의 게시글 목록
GET /posts/:id → 특정 게시글 조회
REST API 예시
// 서버 구현 (Express.js)
const express = require('express');
const app = express();
// 사용자 조회
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json({
id: user.id,
name: user.name,
email: user.email,
createdAt: user.createdAt
// 고정된 응답 구조
});
});
// 게시글 목록 조회
app.get('/api/users/:id/posts', async (req, res) => {
const posts = await db.posts.findByUserId(req.params.id);
res.json(posts);
});
// 클라이언트 사용
// 사용자 프로필 페이지에서 필요한 데이터:
// 1. 사용자 정보
const userResponse = await fetch('/api/users/123');
const user = await userResponse.json();
// 2. 사용자의 게시글
const postsResponse = await fetch('/api/users/123/posts');
const posts = await postsResponse.json();
// 3. 팔로워 수
const followersResponse = await fetch('/api/users/123/followers/count');
const followers = await followersResponse.json();
// → 3번의 HTTP 요청 필요 (Under-fetching 문제)
REST의 Over-fetching과 Under-fetching
// Over-fetching: 필요 이상의 데이터 수신
// 사용자 이름만 필요한데...
GET /api/users/123
// 응답:
{
"id": 123,
"name": "Alice", // ← 이것만 필요
"email": "alice@...", // 불필요
"phone": "010-1234-5678", // 불필요
"address": "서울시...", // 불필요
"createdAt": "2024-01-01", // 불필요
"preferences": { ... }, // 불필요
"lastLoginAt": "..." // 불필요
}
// Under-fetching: 여러 번 요청해야 필요한 데이터 획득
// 소셜 피드를 위해 필요한 것:
// 1. GET /api/posts (게시글 목록)
// 2. GET /api/users/:id (각 게시글 작성자 - N번)
// 3. GET /api/posts/:id/likes (각 게시글 좋아요 수 - N번)
// → N+1 문제
GraphQL
기본 개념
GraphQL은 클라이언트가 필요한 데이터의 구조를 직접 지정하는 쿼리 언어입니다. 단일 엔드포인트(/graphql)에서 모든 요청을 처리합니다.
# GraphQL 스키마 정의
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
followers: [User!]!
followerCount: Int!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
likeCount: Int!
createdAt: String!
}
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
posts: [Post!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
updateUser(id: ID!, name: String): User!
deleteUser(id: ID!): Boolean!
createPost(title: String!, content: String!, authorId: ID!): Post!
}
type Subscription {
newPost: Post!
userOnline(userId: ID!): Boolean!
}
GraphQL 쿼리 예시
# 클라이언트가 필요한 데이터만 지정
# 사용자 프로필 + 최근 게시글 + 팔로워 수 - 단 1번의 요청
query GetUserProfile($userId: ID!) {
user(id: $userId) {
name # 이름만 (email, phone 등 불필요한 필드 제외)
followerCount # 팔로워 수
posts {
id
title
likeCount
createdAt
# content는 필요 없으므로 제외
}
}
}
// 서버 구현 (Apollo Server)
const { ApolloServer, gql } = require('@apollo/server');
const typeDefs = gql`
type User {
id: ID!
name: String!
posts: [Post!]!
}
# ...
`;
const resolvers = {
Query: {
user: async (_, { id }, context) => {
return await context.db.users.findById(id);
},
posts: async () => {
return await context.db.posts.findAll();
}
},
User: {
// 필드 리졸버 - 해당 필드가 요청될 때만 실행
posts: async (user, _, context) => {
return await context.db.posts.findByUserId(user.id);
}
},
Mutation: {
createUser: async (_, { name, email }, context) => {
return await context.db.users.create({ name, email });
}
},
Subscription: {
newPost: {
subscribe: (_, __, { pubsub }) => pubsub.asyncIterator(['NEW_POST'])
}
}
};
// 클라이언트 사용 (Apollo Client)
import { useQuery, useMutation } from '@apollo/client';
const GET_USER_PROFILE = gql`
query GetUserProfile($userId: ID!) {
user(id: $userId) {
name
followerCount
posts {
id
title
likeCount
}
}
}
`;
function UserProfile({ userId }) {
const { loading, error, data } = useQuery(GET_USER_PROFILE, {
variables: { userId }
});
if (loading) return <Spinner />;
if (error) return <Error />;
const { user } = data;
return (
<div>
<h1>{user.name}</h1>
<span>팔로워 {user.followerCount}명</span>
{user.posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
REST vs GraphQL 비교
| 항목 | REST API | GraphQL |
|---|---|---|
| 엔드포인트 | 리소스별 다수 | 단일 /graphql |
| 응답 구조 | 서버에서 고정 | 클라이언트가 지정 |
| Over-fetching | 발생할 수 있음 | 없음 (필요한 것만) |
| Under-fetching | 발생할 수 있음 | 없음 (한 번에 해결) |
| 버전 관리 | /v1/, /v2/ 경로 | 스키마 진화 (deprecated) |
| 캐싱 | HTTP 캐싱 (URL 기반) | 복잡 (POST 요청 기반) |
| 파일 업로드 | 쉬움 (multipart/form-data) | 별도 설정 필요 |
| 실시간 | SSE, WebSocket 별도 구성 | Subscription 내장 |
| 학습 곡선 | 낮음 | 높음 |
| 타입 시스템 | 없음 (OpenAPI로 보완) | 내장 (강력한 타입) |
| 디버깅 | 쉬움 (curl, 브라우저) | GraphiQL/Playground 필요 |
N+1 문제와 DataLoader
GraphQL에서 주의해야 할 N+1 문제:
// ❌ N+1 문제: posts 10개를 가져올 때 author 쿼리가 10번 실행
const resolvers = {
Post: {
author: async (post) => {
return await db.users.findById(post.authorId); // N번 호출!
}
}
};
// ✅ DataLoader로 해결: 배치 처리
const DataLoader = require('dataloader');
const userLoader = new DataLoader(async (userIds) => {
// 한 번의 쿼리로 여러 사용자 조회 (배치)
const users = await db.users.findByIds(userIds);
return userIds.map(id => users.find(u => u.id === id));
});
const resolvers = {
Post: {
author: async (post) => {
return await userLoader.load(post.authorId); // 배치로 처리
}
}
};
각각 적합한 상황
REST API가 적합한 상황
1. 단순한 CRUD 애플리케이션
- 리소스 구조가 단순하고 명확할 때
- 복잡한 데이터 요구사항이 없을 때
2. 캐싱이 중요한 경우
- CDN 캐싱, HTTP 캐싱 활용이 필요할 때
- 공개 API (Public API)
3. 외부 공개 API
- 다양한 언어/플랫폼에서 쉽게 사용 가능
- 문서화가 쉽고 이해하기 쉬움 (Swagger/OpenAPI)
4. 파일 업/다운로드가 빈번한 경우
- multipart/form-data 처리가 간단
5. 팀이 REST에 익숙할 때
- 학습 곡선 없이 빠른 개발 가능
GraphQL이 적합한 상황
1. 복잡한 데이터 요구사항
- 여러 리소스를 조합해야 하는 경우
- Over/Under-fetching 문제가 심각한 경우
2. 다양한 클라이언트 (모바일, 웹, TV 등)
- 각 클라이언트마다 필요한 데이터 구조가 다를 때
- 한 스키마로 다양한 클라이언트 지원
3. 빠른 프론트엔드 개발
- 백엔드 API 변경 없이 프론트엔드 요구사항 충족
- 강력한 타입 시스템으로 자동 완성, 오류 사전 탐지
4. 실시간 기능
- Subscription을 통한 실시간 업데이트
- 채팅, 알림, 라이브 피드 등
5. 마이크로서비스 통합 (Schema Stitching / Federation)
- 여러 서비스의 API를 하나로 통합
하이브리드 접근법
실무에서는 둘을 함께 사용하기도 합니다:
REST API 사용:
- 인증/인가 (/auth/login, /auth/refresh)
- 파일 업로드 (/upload)
- 웹훅 수신
- 외부 API 연동
GraphQL 사용:
- 복잡한 데이터 조회
- 실시간 알림 (Subscription)
- 모바일/웹 클라이언트용 API
정리
| 선택 기준 | REST | GraphQL |
|---|---|---|
| 프로젝트 규모 | 소~중규모 단순 CRUD | 중~대규모 복잡한 데이터 |
| 클라이언트 수 | 단일/소수 클라이언트 | 다양한 클라이언트 |
| 캐싱 요구 | HTTP 캐싱 중요 | 복잡한 캐싱 허용 가능 |
| 팀 숙련도 | REST에 익숙 | GraphQL 학습 의지 있음 |
| 공개 여부 | 공개 API (이해하기 쉬움) | 내부 API |
| 실시간 요구 | 단순 폴링으로 충분 | 진짜 실시간 필요 |
| 개발 속도 | 빠른 초기 개발 | 프론트 자율성 높아 장기적 빠름 |
핵심: REST는 단순하고 이해하기 쉬우며 캐싱에 유리합니다. GraphQL은 클라이언트 주도적 데이터 요청으로 Over/Under-fetching 문제를 해결하지만 복잡성이 증가합니다. 프로젝트 요구사항에 맞게 선택하세요.