REST vs gRPC - 통신 프로토콜 선택
학습 목표
- REST와 gRPC의 핵심 차이 이해
- 각 프로토콜의 장단점 비교
- gRPC 실전 구현
- 프로토콜 선택 가이드
REST API
특징
- HTTP/1.1 기반
- JSON (또는 XML) 사용
- 사람이 읽기 쉬움
- 브라우저 친화적
Express로 REST API 구현
import express from 'express';
const app = express();
app.use(express.json());
// GET: 상품 목록 조회
app.get('/api/products', async (req, res) => {
const products = await productRepo.findAll();
res.json(products);
});
// GET: 상품 상세 조회
app.get('/api/products/:id', async (req, res) => {
const product = await productRepo.findById(req.params.id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
res.json(product);
});
// POST: 상품 생성
app.post('/api/products', async (req, res) => {
const product = await productRepo.create(req.body);
res.status(201).json(product);
});
// PUT: 상품 수정
app.put('/api/products/:id', async (req, res) => {
const product = await productRepo.update(req.params.id, req.body);
res.json(product);
});
// DELETE: 상품 삭제
app.delete('/api/products/:id', async (req, res) => {
await productRepo.delete(req.params.id);
res.status(204).send();
});
app.listen(3000);
REST의 장점
✅ 간단하고 직관적 ✅ 디버깅 쉬움 (cURL, Postman) ✅ 브라우저에서 바로 테스트 가능 ✅ 캐싱 지원 (HTTP Cache) ✅ 풍부한 생태계
REST의 단점
❌ 성능 (JSON 파싱 오버헤드) ❌ HTTP/1.1 제약 (Head-of-line blocking) ❌ Over-fetching/Under-fetching ❌ 타입 안정성 부족
gRPC
특징
- HTTP/2 기반
- Protocol Buffers 사용
- 이진 프로토콜 (빠름)
- 양방향 스트리밍 지원
Protocol Buffers 정의
// product.proto
syntax = "proto3";
package product;
service ProductService {
// Unary RPC: 단순 요청-응답
rpc GetProduct(GetProductRequest) returns (Product);
// Server Streaming: 서버가 여러 응답 전송
rpc ListProducts(ListProductsRequest) returns (stream Product);
// Client Streaming: 클라이언트가 여러 요청 전송
rpc CreateProducts(stream CreateProductRequest) returns (CreateProductsResponse);
// Bidirectional Streaming: 양방향 스트리밍
rpc SearchProducts(stream SearchRequest) returns (stream Product);
}
message GetProductRequest {
string id = 1;
}
message Product {
string id = 1;
string name = 2;
string description = 3;
double price = 4;
int32 stock = 5;
repeated string images = 6;
}
message ListProductsRequest {
int32 limit = 1;
int32 offset = 2;
}
message CreateProductRequest {
string name = 1;
double price = 2;
int32 stock = 3;
}
message CreateProductsResponse {
int32 created_count = 1;
}
message SearchRequest {
string query = 1;
}
gRPC 서버 구현 (Node.js)
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
const PROTO_PATH = './product.proto';
const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const proto = grpc.loadPackageDefinition(packageDefinition).product;
// Service 구현
const server = new grpc.Server();
server.addService(proto.ProductService.service, {
// Unary RPC
getProduct: async (call, callback) => {
const { id } = call.request;
try {
const product = await productRepo.findById(id);
if (!product) {
return callback({
code: grpc.status.NOT_FOUND,
message: 'Product not found',
});
}
callback(null, product);
} catch (error) {
callback({
code: grpc.status.INTERNAL,
message: error.message,
});
}
},
// Server Streaming
listProducts: async (call) => {
const { limit, offset } = call.request;
const products = await productRepo.findAll({ limit, offset });
for (const product of products) {
call.write(product);
}
call.end();
},
// Client Streaming
createProducts: async (call, callback) => {
const products = [];
call.on('data', (request) => {
products.push(request);
});
call.on('end', async () => {
await productRepo.insertMany(products);
callback(null, {
created_count: products.length,
});
});
},
// Bidirectional Streaming
searchProducts: async (call) => {
call.on('data', async (request) => {
const results = await productRepo.search(request.query);
for (const product of results) {
call.write(product);
}
});
call.on('end', () => {
call.end();
});
},
});
server.bindAsync(
'0.0.0.0:50051',
grpc.ServerCredentials.createInsecure(),
() => {
server.start();
console.log('gRPC server running on port 50051');
}
);
gRPC 클라이언트 구현
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
const PROTO_PATH = './product.proto';
const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const proto = grpc.loadPackageDefinition(packageDefinition).product;
const client = new proto.ProductService(
'localhost:50051',
grpc.credentials.createInsecure()
);
// Unary RPC 호출
function getProduct(id: string): Promise<Product> {
return new Promise((resolve, reject) => {
client.getProduct({ id }, (error, response) => {
if (error) {
reject(error);
} else {
resolve(response);
}
});
});
}
// Server Streaming 호출
function listProducts(limit: number, offset: number) {
const call = client.listProducts({ limit, offset });
call.on('data', (product) => {
console.log('Received:', product);
});
call.on('end', () => {
console.log('Stream ended');
});
call.on('error', (error) => {
console.error('Error:', error);
});
}
// 사용 예제
async function main() {
try {
const product = await getProduct('123');
console.log('Product:', product);
listProducts(10, 0);
} catch (error) {
console.error('Error:', error);
}
}
main();
gRPC의 장점
✅ 고성능 (Protocol Buffers + HTTP/2) ✅ 타입 안정성 (코드 생성) ✅ 양방향 스트리밍 ✅ 작은 페이로드 크기 ✅ 다국어 지원 (코드 생성)
gRPC의 단점
❌ 브라우저 직접 호출 불가 (gRPC-Web 필요) ❌ 디버깅 어려움 (이진 프로토콜) ❌ Learning Curve ❌ Protobuf 스키마 관리 필요
성능 비교
페이로드 크기
동일한 Product 객체 전송 시:
JSON (REST): 450 bytes
Protobuf (gRPC): 120 bytes
→ gRPC가 약 73% 작음
처리 속도
10,000개 Product 목록 조회:
REST: 250ms
gRPC: 80ms
→ gRPC가 약 3배 빠름
Latency
단일 요청 Round-trip:
REST (HTTP/1.1): 50ms
gRPC (HTTP/2): 20ms
실전 예제: REST + gRPC 혼용
시나리오
┌─────────────┐
│ Browser │
│ (Web App) │
└──────┬──────┘
│ REST API
↓
┌─────────────┐
│ API Gateway │
└──────┬──────┘
│ gRPC
↓
┌──────────────┬──────────────┬──────────────┐
│ Product │ Order │ Payment │
│ Service │ Service │ Service │
└──────────────┴──────────────┴──────────────┘
↕ gRPC ↕
패턴:
- 외부 클라이언트 (브라우저) → REST
- 내부 서비스 간 통신 → gRPC
API Gateway에서 REST → gRPC 변환
import express from 'express';
import * as grpc from '@grpc/grpc-js';
const app = express();
// gRPC 클라이언트 초기화
const productClient = new proto.ProductService(
'product-service:50051',
grpc.credentials.createInsecure()
);
// REST 엔드포인트 → gRPC 호출
app.get('/api/products/:id', async (req, res) => {
try {
// gRPC 호출
const product = await new Promise((resolve, reject) => {
productClient.getProduct({ id: req.params.id }, (error, response) => {
if (error) reject(error);
else resolve(response);
});
});
// REST 응답 반환
res.json(product);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(8080);
프로토콜 선택 가이드
REST를 선택하는 경우
✅ 브라우저 클라이언트 ✅ 공개 API (third-party 통합) ✅ 간단한 CRUD 작업 ✅ 디버깅 용이성 중요 ✅ HTTP 캐싱 활용
예시:
- 모바일 앱 API
- Partner API
- Webhook
gRPC를 선택하는 경우
✅ 마이크로서비스 간 내부 통신 ✅ 고성능 요구사항 ✅ 실시간 스트리밍 필요 ✅ 다국어 클라이언트 ✅ 타입 안정성 중요
예시:
- Order Service ↔ Inventory Service
- 실시간 채팅
- 로그 수집
- IoT 데이터 스트리밍
하이브리드 접근법
// Service Interface 정의
interface ProductService {
getProduct(id: string): Promise<Product>;
listProducts(limit: number): Promise<Product[]>;
}
// REST 구현
class RestProductService implements ProductService {
async getProduct(id: string) {
const res = await fetch(`http://api/products/${id}`);
return res.json();
}
async listProducts(limit: number) {
const res = await fetch(`http://api/products?limit=${limit}`);
return res.json();
}
}
// gRPC 구현
class GrpcProductService implements ProductService {
private client: any;
async getProduct(id: string) {
return new Promise((resolve, reject) => {
this.client.getProduct({ id }, (error, response) => {
if (error) reject(error);
else resolve(response);
});
});
}
async listProducts(limit: number) {
// Server streaming → Promise<Array>로 변환
return new Promise((resolve, reject) => {
const products = [];
const call = this.client.listProducts({ limit, offset: 0 });
call.on('data', (product) => products.push(product));
call.on('end', () => resolve(products));
call.on('error', (error) => reject(error));
});
}
}
// 환경에 따라 선택
const productService: ProductService = process.env.USE_GRPC
? new GrpcProductService()
: new RestProductService();
핵심 정리
- REST: 간단, 디버깅 쉬움, 브라우저 친화적
- gRPC: 고성능, 타입 안정성, 스트리밍 지원
- 외부 API는 REST, 내부 통신은 gRPC 추천
- API Gateway에서 프로토콜 변환 가능
- 상황에 맞는 프로토콜 선택이 핵심