전체 목록
네트워크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 APIGraphQL
엔드포인트리소스별 다수단일 /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

정리

선택 기준RESTGraphQL
프로젝트 규모소~중규모 단순 CRUD중~대규모 복잡한 데이터
클라이언트 수단일/소수 클라이언트다양한 클라이언트
캐싱 요구HTTP 캐싱 중요복잡한 캐싱 허용 가능
팀 숙련도REST에 익숙GraphQL 학습 의지 있음
공개 여부공개 API (이해하기 쉬움)내부 API
실시간 요구단순 폴링으로 충분진짜 실시간 필요
개발 속도빠른 초기 개발프론트 자율성 높아 장기적 빠름

핵심: REST는 단순하고 이해하기 쉬우며 캐싱에 유리합니다. GraphQL은 클라이언트 주도적 데이터 요청으로 Over/Under-fetching 문제를 해결하지만 복잡성이 증가합니다. 프로젝트 요구사항에 맞게 선택하세요.