API 설계 패턴 비교 분석 2025
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로 시작하고, 특별한 요구사항(실시간, 유연한 쿼리, 고성능)이 있을 때 다른 패턴을 고려하세요. 여러 패턴을 조합하는 하이브리드 접근법도 좋은 선택입니다.