API 설계 패턴 비교 분석 2025

18 min read
compareapirestgraphqlgrpcwebsocketapi-designmicroservices

REST vs GraphQL vs gRPC vs WebSocket - API 설계 패턴 선택 가이드

🔌 API 설계 패턴 비교 분석 2025

프로젝트 요구사항에 따른 최적의 API 설계 패턴 선택 가이드


📊 개요

비교 대상

  • REST: Representational State Transfer
  • GraphQL: Facebook의 쿼리 언어
  • gRPC: Google의 RPC 프레임워크
  • WebSocket: 양방향 실시간 통신
  • JSON-RPC: 경량 RPC 프로토콜
  • SOAP: 엔터프라이즈 표준 (레거시)

평가 기준

  • 개발 생산성
  • 성능 (처리량, 지연시간)
  • 유연성
  • 타입 안전성
  • 클라이언트 복잡도
  • 생태계 성숙도

📈 상세 비교표

핵심 특성 비교

| 특성 | REST | GraphQL | gRPC | WebSocket | | --------------- | ---------------- | ---------------- | ---------------- | --------- | | 프로토콜 | HTTP/HTTPS | HTTP | HTTP/2 | WS/WSS | | 데이터 형식 | JSON/XML | JSON | Protocol Buffers | Any | | 통신 방식 | Request-Response | Request-Response | 양방향 스트림 | 양방향 | | 스키마 정의 | OpenAPI | GraphQL Schema | Proto files | 없음 | | 타입 안전성 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐ | | 성능 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | | 복잡도 | 낮음 | 중간 | 높음 | 중간 |

장단점 분석

| 항목 | REST | GraphQL | gRPC | WebSocket | | -------- | -------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------ | | 장점 | • 단순함• 캐싱 용이• 표준화 | • 유연한 쿼리• Over/Under-fetching 해결• 단일 엔드포인트 | • 고성능• 양방향 스트리밍• 다국어 지원 | • 실시간• 양방향• 낮은 오버헤드 | | 단점 | • Over-fetching• N+1 문제• 버전 관리 | • 캐싱 복잡• 파일 업로드 제한• 학습 곡선 | • 브라우저 제한• 디버깅 어려움• 프록시 문제 | • 상태 관리• 확장성 제한• HTTP 기능 부재 |


💼 사용 사례별 구현

🛍️ 이커머스 API (REST)

추천: RESTful API

// Express.js REST API
import express from "express";
import { validate } from "express-validation";

const app = express();

// 상품 목록 조회 (페이지네이션, 필터링)
app.get("/api/v1/products", validate(productQuerySchema), async (req, res) => {
  const {
    page = 1,
    limit = 20,
    category,
    minPrice,
    maxPrice,
    sort,
  } = req.query;

  const products = await Product.findAll({
    where: {
      ...(category && { category }),
      ...(minPrice && { price: { $gte: minPrice } }),
      ...(maxPrice && { price: { $lte: maxPrice } }),
    },
    limit,
    offset: (page - 1) * limit,
    order: [[sort || "createdAt", "DESC"]],
  });

  const total = await Product.count({
    where: {/* same filters */},
  });

  res.json({
    data: products,
    pagination: {
      page,
      limit,
      total,
      pages: Math.ceil(total / limit),
    },
    links: {
      self: `/api/v1/products?page=${page}`,
      next:
        page < Math.ceil(total / limit)
          ? `/api/v1/products?page=${page + 1}`
          : null,
      prev: page > 1 ? `/api/v1/products?page=${page - 1}` : null,
    },
  });
});

// 주문 생성 (트랜잭션)
app.post(
  "/api/v1/orders",
  authenticate,
  validate(orderSchema),
  async (req, res) => {
    const transaction = await sequelize.transaction();

    try {
      // 주문 생성
      const order = await Order.create(
        {
          userId: req.user.id,
          items: req.body.items,
          shippingAddress: req.body.shippingAddress,
          totalAmount: calculateTotal(req.body.items),
        },
        { transaction },
      );

      // 재고 차감
      for (const item of req.body.items) {
        await Product.decrement("stock", {
          by: item.quantity,
          where: { id: item.productId },
          transaction,
        });
      }

      // 결제 처리
      const payment = await processPayment(
        {
          orderId: order.id,
          amount: order.totalAmount,
          method: req.body.paymentMethod,
        },
        { transaction },
      );

      await transaction.commit();

      res.status(201).json({
        data: order,
        links: {
          self: `/api/v1/orders/${order.id}`,
          payment: `/api/v1/payments/${payment.id}`,
        },
      });
    } catch (error) {
      await transaction.rollback();
      res.status(400).json({ error: error.message });
    }
  },
);

// API 버전 관리
app.use("/api/v1", v1Routes);
app.use("/api/v2", v2Routes);

📱 모바일 앱 백엔드 (GraphQL)

추천: GraphQL

// Apollo Server GraphQL
import { ApolloServer, gql } from "apollo-server";
import { buildFederatedSchema } from "@apollo/federation";

// 스키마 정의
const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
    profile: Profile!
    posts(limit: Int = 10, offset: Int = 0): [Post!]!
    friends: [User!]!
  }

  type Profile {
    bio: String
    avatar: String
    location: Location
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    comments(limit: Int = 10): [Comment!]!
    likes: Int!
    createdAt: DateTime!
  }

  type Query {
    me: User
    user(id: ID!): User
    feed(cursor: String, limit: Int = 20): FeedConnection!
    searchUsers(query: String!): [User!]!
  }

  type Mutation {
    createPost(input: CreatePostInput!): Post!
    likePost(postId: ID!): Post!
    followUser(userId: ID!): User!
    updateProfile(input: UpdateProfileInput!): Profile!
  }

  type Subscription {
    postAdded(userId: ID!): Post!
    messageReceived: Message!
  }
`;

// 리졸버 구현
const resolvers = {
  Query: {
    me: (_, __, { user }) => getUserById(user.id),

    feed: async (_, { cursor, limit }, { user }) => {
      // 커서 기반 페이지네이션
      const posts = await getFeedForUser(user.id, cursor, limit);

      return {
        edges: posts.map((post) => ({
          node: post,
          cursor: encodeCursor(post.id),
        })),
        pageInfo: {
          hasNextPage: posts.length === limit,
          endCursor: posts.length
            ? encodeCursor(posts[posts.length - 1].id)
            : null,
        },
      };
    },
  },

  User: {
    // N+1 문제 해결 (DataLoader)
    posts: async (user, { limit, offset }, { loaders }) => {
      return loaders.userPosts.load({ userId: user.id, limit, offset });
    },

    friends: async (user, _, { loaders }) => {
      return loaders.userFriends.load(user.id);
    },
  },

  Mutation: {
    createPost: async (_, { input }, { user }) => {
      const post = await Post.create({
        ...input,
        authorId: user.id,
      });

      // 구독자에게 알림
      pubsub.publish("POST_ADDED", { postAdded: post });

      return post;
    },
  },

  Subscription: {
    postAdded: {
      subscribe: withFilter(
        () => pubsub.asyncIterator("POST_ADDED"),
        (payload, variables) => {
          return payload.postAdded.authorId === variables.userId;
        },
      ),
    },
  },
};

// DataLoader로 N+1 문제 해결
const createLoaders = () => ({
  userPosts: new DataLoader(async (keys) => {
    const posts = await Post.findAll({
      where: { userId: keys.map((k) => k.userId) },
      limit: keys[0].limit,
      offset: keys[0].offset,
    });

    return keys.map((key) =>
      posts.filter((post) => post.userId === key.userId),
    );
  }),
});

🎮 마이크로서비스 통신 (gRPC)

추천: gRPC

// service.proto
syntax = "proto3";

package game;

service GameService {
  // 단방향 RPC
  rpc GetPlayer(GetPlayerRequest) returns (Player);

  // 서버 스트리밍 RPC
  rpc ListPlayers(ListPlayersRequest) returns (stream Player);

  // 클라이언트 스트리밍 RPC
  rpc RecordGameplay(stream GameplayEvent) returns (GameplaySummary);

  // 양방향 스트리밍 RPC
  rpc GameStream(stream GameAction) returns (stream GameUpdate);
}

message Player {
  string id = 1;
  string name = 2;
  int32 level = 3;
  int32 experience = 4;
  repeated Item inventory = 5;
  PlayerStats stats = 6;
}

message GameAction {
  string player_id = 1;
  oneof action {
    MoveAction move = 2;
    AttackAction attack = 3;
    UseItemAction use_item = 4;
  }
  int64 timestamp = 5;
}
// Go 서버 구현
type gameServer struct {
    pb.UnimplementedGameServiceServer
    playerStore PlayerStore
}

// 양방향 스트리밍 구현
func (s *gameServer) GameStream(stream pb.GameService_GameStreamServer) error {
    playerId := extractPlayerId(stream.Context())

    // 플레이어별 게임 상태
    gameState := s.getOrCreateGameState(playerId)

    // 고루틴으로 클라이언트 액션 처리
    go func() {
        for {
            action, err := stream.Recv()
            if err == io.EOF {
                return
            }
            if err != nil {
                log.Printf("Error receiving action: %v", err)
                return
            }

            // 게임 로직 처리
            update := s.processAction(gameState, action)

            // 다른 플레이어에게 브로드캐스트
            s.broadcastUpdate(update)
        }
    }()

    // 게임 업데이트 전송
    for update := range gameState.Updates {
        if err := stream.Send(update); err != nil {
            return err
        }
    }

    return nil
}

// 클라이언트 (Python)
import grpc
import game_pb2
import game_pb2_grpc

async def play_game():
    async with grpc.aio.insecure_channel('localhost:50051') as channel:
        stub = game_pb2_grpc.GameServiceStub(channel)

        # 양방향 스트리밍
        stream = stub.GameStream()

        # 액션 전송 태스크
        async def send_actions():
            while True:
                action = get_player_action()
                await stream.write(action)

        # 업데이트 수신 태스크
        async def receive_updates():
            async for update in stream:
                render_game_update(update)

        await asyncio.gather(send_actions(), receive_updates())

💬 실시간 채팅 (WebSocket)

추천: WebSocket + REST 하이브리드

// Socket.io + REST API
import { Server } from "socket.io";
import express from "express";

const app = express();
const io = new Server(server);

// REST API - 채팅방 관리
app.post("/api/rooms", authenticate, async (req, res) => {
  const room = await Room.create({
    name: req.body.name,
    creatorId: req.user.id,
    members: [req.user.id],
  });

  res.json({ data: room });
});

// REST API - 메시지 히스토리
app.get("/api/rooms/:roomId/messages", authenticate, async (req, res) => {
  const messages = await Message.findAll({
    where: { roomId: req.params.roomId },
    include: [{ model: User, as: "sender" }],
    order: [["createdAt", "DESC"]],
    limit: 50,
  });

  res.json({ data: messages });
});

// WebSocket - 실시간 메시징
io.use(socketAuth);

io.on("connection", (socket) => {
  console.log("User connected:", socket.userId);

  // 룸 참가
  socket.on("join-room", async (roomId) => {
    // 권한 확인
    const hasAccess = await checkRoomAccess(socket.userId, roomId);
    if (!hasAccess) {
      return socket.emit("error", "Access denied");
    }

    socket.join(roomId);

    // 참가 알림
    socket.to(roomId).emit("user-joined", {
      userId: socket.userId,
      timestamp: new Date(),
    });
  });

  // 메시지 전송
  socket.on("send-message", async (data) => {
    const { roomId, content, attachments } = data;

    // 메시지 저장 (REST API와 공유)
    const message = await Message.create({
      roomId,
      senderId: socket.userId,
      content,
      attachments,
    });

    // 실시간 브로드캐스트
    io.to(roomId).emit("new-message", {
      ...message.toJSON(),
      sender: await message.getSender(),
    });
  });

  // 타이핑 인디케이터
  socket.on("typing", ({ roomId, isTyping }) => {
    socket.to(roomId).emit("user-typing", {
      userId: socket.userId,
      isTyping,
    });
  });
});

🏢 실제 기업 사례

REST 사용

  • Twitter: REST API v2
  • GitHub: REST API v3
  • Stripe: 결제 API
  • 카카오: 오픈 API
  • 네이버: 오픈 API

GraphQL 사용

  • Facebook: 전체 API
  • GitHub: GraphQL API v4
  • Shopify: Storefront API
  • 토스: 일부 내부 API

gRPC 사용

  • Google: 내부 서비스
  • Netflix: 마이크로서비스
  • Uber: 서비스 통신
  • 라인: 메시징 서버

WebSocket 사용

  • Slack: 실시간 메시징
  • Discord: 채팅/음성
  • Figma: 실시간 협업
  • 당근마켓: 채팅

🔄 하이브리드 접근법

REST + GraphQL

// 공존 전략
app.use("/api/rest", restRoutes);
app.use("/graphql", apolloServer.getMiddleware());

// REST에서 GraphQL 재사용
app.get("/api/rest/users/:id", async (req, res) => {
  const result = await apolloServer.executeOperation({
    query: `
      query GetUser($id: ID!) {
        user(id: $id) {
          id
          name
          email
        }
      }
    `,
    variables: { id: req.params.id },
  });

  res.json(result.data.user);
});

gRPC + REST Gateway

# gRPC Gateway 설정
type: google.api.Service
config_version: 3

http:
  rules:
    - selector: game.GameService.GetPlayer
      get: /v1/players/{player_id}
    - selector: game.GameService.ListPlayers
      get: /v1/players

💰 비용 및 성능 비교

성능 벤치마크

| 메트릭 | REST | GraphQL | gRPC | WebSocket | | --------------- | ---- | ------- | ---- | --------- | | 요청/초 | 10K | 8K | 50K | 30K | | 지연시간 | 10ms | 15ms | 2ms | 1ms | | 대역폭 사용 | 100% | 60% | 30% | 50% | | CPU 사용률 | 낮음 | 중간 | 낮음 | 중간 |

개발 비용

  • REST: 가장 낮음 (표준화)
  • GraphQL: 중간 (초기 설정)
  • gRPC: 높음 (도구 체인)
  • WebSocket: 중간 (상태 관리)

🎯 선택 가이드

REST 선택 시

✅ 공개 API
✅ 간단한 CRUD
✅ 캐싱 중요
✅ 파일 업/다운로드
❌ 복잡한 데이터 요구
❌ 실시간 기능

GraphQL 선택 시

✅ 모바일 앱 백엔드
✅ 복잡한 데이터 모델
✅ 다양한 클라이언트
✅ Rapid development
❌ 파일 처리
❌ 단순 CRUD

gRPC 선택 시

✅ 마이크로서비스
✅ 고성능 필요
✅ 타입 안전성
✅ 스트리밍 데이터
❌ 브라우저 직접 호출
❌ 공개 API

WebSocket 선택 시

✅ 실시간 기능
✅ 양방향 통신
✅ 라이브 업데이트
✅ 협업 도구
❌ 단순 요청-응답
❌ 캐싱 필요


📚 추가 리소스

명세 및 도구

한국 기술 블로그

API 테스트 도구

  • REST: Postman, Insomnia
  • GraphQL: GraphQL Playground, Altair
  • gRPC: BloomRPC, grpcurl
  • WebSocket: wscat, Socket.io Client

💡 핵심 조언: API 설계는 "최신 기술"이 아닌 "적합한 기술"을 선택하는 것입니다. 대부분의 경우 REST로 시작하고, 특별한 요구사항(실시간, 유연한 쿼리, 고성능)이 있을 때 다른 패턴을 고려하세요. 여러 패턴을 조합하는 하이브리드 접근법도 좋은 선택입니다.

Found this helpful? Share it with others!
Tweet

🔗 Related Content

You might also be interested in these articles

🏢 company

🏢 Canva 기술 스택 분석

캔바가 1억 명 이상의 사용자에게 쉽고 강력한 디자인 도구를 제공하는 기술 스택 심층 분석 - Java, TypeScript, AWS로 구축한 대규모 디자인 플랫폼

32 min read
canva, java +10
Read more
🏢 company

🏢 Uber 기술 스택 분석

우버가 매일 2,500만 건의 라이드를 실시간으로 매칭하는 기술 스택 심층 분석 - Go, Java, Node.js로 구축한 글로벌 실시간 마켓플레이스

15 min read
uber, microservices +12
Read more
🏗️ stack

🏢 Enterprise Microservices Stack

대규모 트래픽과 복잡한 비즈니스 로직을 위한 마이크로서비스 아키텍처 - Go, gRPC, Kubernetes로 구축하는 확장 가능한 시스템

14 min read
go, grpc +13
Read more

Found this helpful?

Help us improve this content by contributing on GitHub or sharing your feedback with the community.