7강 45분

REST vs gRPC - 통신 프로토콜 선택

마이크로서비스 간 통신에서 REST API와 gRPC의 차이점을 이해하고, 상황에 맞는 프로토콜을 선택하는 방법을 배웁니다.

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에서 프로토콜 변환 가능
  • 상황에 맞는 프로토콜 선택이 핵심