마이크로서비스 아키텍처(MSA)를 설계할 때 서비스 간 통신 프로토콜 선택은 전체 시스템 성능과 운영 편의성을 좌우하는 핵심 결정입니다. REST는 수십 년간 표준으로 군림해왔지만, gRPC는 구글이 내부 RPC 프레임워크를 오픈소스화하면서 고성능 서비스 간 통신의 유력한 대안으로 떠올랐습니다. 두 프로토콜은 서로 다른 트레이드오프를 갖고 있으며, “무엇이 더 좋다”는 단순한 답 대신 “어떤 상황에 무엇이 맞는가”를 이해하는 것이 실무에서 훨씬 중요합니다. 이 글에서는 직렬화 방식, 스트리밍 지원, 브라우저 호환성, 생태계 성숙도 등 여러 축에서 두 프로토콜을 비교하고, 실제 마이그레이션 시 고려해야 할 사항을 정리합니다.

프로토콜 기초: 무엇이 어떻게 다른가

REST(Representational State Transfer)는 HTTP/1.1 위에서 동작하며, JSON 또는 XML을 페이로드로 사용합니다. 리소스 중심 설계를 권장하고, HTTP 메서드(GET, POST, PUT, DELETE)와 상태 코드로 의미를 표현합니다. 클라이언트-서버 간 계약은 OpenAPI(Swagger) 스펙으로 문서화하지만, 강제성은 런타임이 아닌 코드 리뷰 레벨에 머뭅니다.

gRPC는 HTTP/2를 전송 계층으로 사용하고, Protocol Buffers(proto3)를 인터페이스 정의 언어(IDL)이자 직렬화 포맷으로 사용합니다. 서비스와 메시지 스키마를 .proto 파일에 명시적으로 정의하며, 코드 생성 도구가 다양한 언어의 클라이언트·서버 스텁을 자동으로 만들어 줍니다. 계약 위반은 컴파일 타임에 감지됩니다.

// user.proto — gRPC 서비스 정의 예시
syntax = "proto3";

package user.v1;

option go_package = "github.com/example/userservice/gen/user/v1";

message GetUserRequest {
  string user_id = 1;
}

message GetUserResponse {
  string user_id    = 1;
  string name       = 2;
  string email      = 3;
  int64  created_at = 4;  // Unix timestamp (UTC)
}

service UserService {
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
  rpc ListUsers(ListUsersRequest) returns (stream GetUserResponse);
  rpc UpdateUser(UpdateUserRequest) returns (GetUserResponse);
}

위 .proto 파일 하나로 Go, Java, Python, TypeScript 등 어떤 언어의 클라이언트·서버 코드도 자동 생성할 수 있습니다. 이 계약 우선(contract-first) 방식은 MSA 환경에서 팀 간 인터페이스 오해를 줄이는 데 큰 이점을 줍니다.

성능 비교: 직렬화·전송 계층의 차이

Protocol Buffers는 바이너리 직렬화 포맷입니다. JSON과 달리 필드명을 전송하지 않고 필드 번호(tag)를 사용하며, varint 인코딩으로 정수를 압축합니다. 일반적으로 같은 데이터를 JSON 대비 3~5배 작은 페이로드로 표현하고, 파싱 속도도 더 빠릅니다. 예를 들어 사용자 객체 1,000개를 직렬화할 때 JSON은 약 180 KB인 반면, protobuf는 약 45 KB 수준으로 알려져 있습니다(데이터 특성에 따라 편차 있음).

HTTP/2 전송 계층도 중요한 요소입니다. HTTP/1.1은 요청당 하나의 TCP 연결을 점유하거나, 파이프라이닝을 사용하더라도 HOL 블로킹 문제가 있습니다. HTTP/2는 하나의 TCP 연결 위에서 다중 스트림을 동시에 처리(멀티플렉싱)하고, 헤더를 HPACK으로 압축합니다. MSA 환경에서 서비스 A가 서비스 B를 초당 수천 번 호출하는 시나리오에서 이 차이는 뚜렷하게 나타납니다.

항목 REST (JSON/HTTP1.1) gRPC (Protobuf/HTTP2)
직렬화 포맷 JSON (텍스트, 가독성 높음) Protocol Buffers (바이너리, 컴팩트)
페이로드 크기 상대적으로 큼 (필드명 반복) 일반적으로 JSON 대비 30~70% 감소
파싱 속도 보통 (텍스트 파싱 오버헤드) 빠름 (바이너리 디코딩)
전송 프로토콜 HTTP/1.1 (주로) HTTP/2 (필수)
연결 재사용 Keep-Alive (제한적) 멀티플렉싱 (효율적)
스트리밍 지원 제한적 (SSE, WebSocket 별도) 네이티브 (단방향·양방향 스트리밍)
브라우저 지원 완벽 지원 gRPC-Web 레이어 필요
인터페이스 계약 OpenAPI (선택적, 런타임 미강제) .proto (필수, 컴파일 타임 강제)
코드 생성 선택적 (codegen 도구 별도) protoc + 언어별 플러그인 기본 제공
디버깅 편의성 curl/Postman 등으로 쉬움 grpcurl, grpc-ui 필요
에러 표현 HTTP 상태 코드 (표준화됨) gRPC 상태 코드 (별도 체계)
생태계·학습 비용 성숙, 낮음 성장 중, 중간

스트리밍과 양방향 통신: gRPC의 강점

gRPC는 네 가지 통신 패턴을 지원합니다. 단순 RPC(Unary), 서버 스트리밍, 클라이언트 스트리밍, 양방향 스트리밍입니다. REST는 기본적으로 요청-응답 패턴만 지원하며, 실시간 스트리밍을 위해 Server-Sent Events(SSE)나 WebSocket 같은 별도 기술을 추가해야 합니다.

// 양방향 스트리밍 예시 — 실시간 채팅 서비스
service ChatService {
  // 클라이언트와 서버가 동시에 메시지를 주고받음
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

message ChatMessage {
  string room_id   = 1;
  string sender_id = 2;
  string content   = 3;
  int64  timestamp = 4;
}

서버 스트리밍은 대량 데이터를 청크 단위로 전송할 때 유용합니다. 예를 들어 ML 모델 서빙 서비스에서 토큰 단위로 추론 결과를 스트리밍하거나, 로그 집계 서비스에서 이벤트를 실시간으로 구독하는 시나리오가 대표적입니다. REST로 같은 기능을 구현하려면 SSE를 사용해야 하는데, 이는 단방향(서버→클라이언트)만 지원하고 헤더 오버헤드가 있습니다.

반면 MSA에서 외부 게이트웨이 레이어에 gRPC를 바로 노출하면 문제가 생깁니다. 브라우저는 HTTP/2 프레이밍을 직접 제어할 수 없어 gRPC를 네이티브로 호출하지 못합니다. 이를 해결하기 위해 gRPC-Web 프록시(Envoy, grpc-gateway 등)를 앞에 두거나, 외부 API는 REST로 노출하고 내부 서비스 간에만 gRPC를 사용하는 하이브리드 전략이 일반적입니다.

언제 무엇을 선택할 것인가

두 프로토콜 중 하나를 선택할 때는 다음 기준으로 판단합니다.

gRPC가 적합한 상황

  • 폴리글랏 MSA 환경에서 서비스 간 내부 통신: 강타입 계약이 팀 간 오해를 방지
  • 고빈도·저지연이 요구되는 내부 API: 물류 트래킹, 실시간 가격 계산, ML 추론
  • 스트리밍이 핵심인 시나리오: 로그 수집, IoT 데이터 파이프라인, 실시간 알림
  • 대역폭이 제한된 환경(모바일 네트워크, 엣지 컴퓨팅): 컴팩트 페이로드 이점

REST가 적합한 상황

  • 외부 퍼블릭 API: 다양한 클라이언트 생태계, 브라우저 직접 호출, curl 디버깅
  • 단순 CRUD 중심의 서비스: 자원 모델이 명확하고 스트리밍 불필요
  • 팀 gRPC 경험이 부족한 초기 단계: 학습 비용 최소화
  • 서드파티 연동이 많은 경우: 파트너사가 OpenAPI 스펙 요구

실무에서 가장 많이 사용하는 패턴은 하이브리드 아키텍처입니다. API 게이트웨이는 REST/HTTP로 외부에 노출하고, 게이트웨이 뒤의 내부 서비스 간 통신은 gRPC로 구성합니다. 이렇게 하면 외부 클라이언트 호환성과 내부 성능을 동시에 확보할 수 있습니다.

마이그레이션 고려사항

기존 REST 서비스를 gRPC로 마이그레이션하거나 두 프로토콜을 혼용할 때 주의해야 할 포인트를 정리합니다.

1. 스키마 설계와 하위 호환성
Protocol Buffers는 필드 번호를 기반으로 하므로, 한 번 배포된 필드 번호는 절대 변경하거나 재사용해서는 안 됩니다. 필드를 삭제할 때는 reserved 키워드로 예약해두고, 새 필드는 항상 신규 번호를 부여합니다. proto3는 모든 필드가 기본값을 가지므로 선택적 필드 처리에 주의가 필요합니다.

// 필드 삭제 시 하위 호환 처리
message UserProfile {
  reserved 3, 4;          // 이전에 사용하던 phone, address 필드 번호 예약
  reserved "phone", "address";  // 이름도 예약

  string user_id    = 1;
  string name       = 2;
  string email      = 5;  // 새 필드는 새 번호
}

2. 에러 처리 체계 통일
REST는 HTTP 상태 코드(404, 400, 500 등)로 에러를 표현하지만, gRPC는 자체 상태 코드 체계(NOT_FOUND, INVALID_ARGUMENT, INTERNAL 등 16가지)를 사용합니다. 게이트웨이 레이어에서 두 체계를 매핑하는 규칙을 문서화하고, 에러 세부 정보는 google.rpc.Status의 details 필드를 활용합니다.

3. 서비스 메시와 관찰 가능성
Istio나 Linkerd 같은 서비스 메시는 gRPC를 네이티브로 지원하며, HTTP/2 레벨에서 트래픽을 관찰합니다. gRPC는 OpenTelemetry와 연동할 때 인터셉터(Interceptor) 패턴으로 분산 트레이싱을 추가합니다. 기존 REST 미들웨어 기반 로깅·메트릭 수집 파이프라인을 gRPC 인터셉터 방식으로 재구성해야 합니다.

4. 점진적 전환 전략
Strangler Fig 패턴을 활용해 일부 엔드포인트부터 gRPC로 전환하고, grpc-gateway를 사용해 동일 .proto 파일에서 REST와 gRPC 엔드포인트를 동시에 노출하는 방법이 실용적입니다. 이렇게 하면 클라이언트를 한꺼번에 마이그레이션하지 않고도 내부 구현을 gRPC로 점진적으로 이전할 수 있습니다.

실전 팁

  • grpcurl 활용: curl의 gRPC 버전. grpcurl -plaintext localhost:50051 list로 서비스 목록 확인, grpcurl -d '{"user_id":"123"}' localhost:50051 user.v1.UserService/GetUser로 즉시 테스트
  • 프로토 파일 버전 관리: .proto 파일을 별도 리포지토리(Buf Schema Registry 활용)로 관리하면 팀 간 계약 공유와 버전 추적이 편리
  • gRPC Health Checking Protocol: grpc.health.v1.Health 서비스를 구현해두면 로드 밸런서와 오케스트레이터(k8s)가 헬스체크를 표준화된 방식으로 수행
  • connection pool 관리: HTTP/2 멀티플렉싱 덕분에 gRPC는 서비스당 하나의 채널(Channel)만 생성해도 충분한 경우가 많음. 과도한 채널 생성은 오히려 오버헤드
  • 타임아웃과 데드라인: gRPC는 데드라인(Deadline) 전파를 네이티브로 지원. 컨텍스트에 데드라인을 설정하면 호출 체인 전체에 전파되어 분산 시스템의 폭포형 지연을 방지

마무리

gRPC와 REST는 서로를 대체하는 관계가 아니라 보완하는 관계입니다. gRPC는 성능이 중요한 내부 서비스 간 통신과 스트리밍 시나리오에서 강력한 이점을 제공하고, REST는 퍼블릭 API와 범용 클라이언트 호환성이 요구되는 영역에서 여전히 최적의 선택입니다. 실제 MSA 설계에서 “내부는 gRPC, 외부는 REST”라는 하이브리드 전략이 많은 엔지니어링 팀이 수렴하는 실용적인 결론입니다. 중요한 것은 프로토콜 자체보다 명확한 인터페이스 계약, 일관된 에러 처리, 그리고 관찰 가능성을 확보하는 것입니다.

자주 묻는 질문

Q. gRPC를 사용하면 REST 대비 항상 빠른가요?
A. 반드시 그렇지는 않습니다. 페이로드가 매우 작거나(수십 바이트), 호출 빈도가 낮은 경우에는 직렬화 오버헤드 차이가 무시할 수준입니다. 성능 이점은 대용량 페이로드, 고빈도 호출, 스트리밍 시나리오에서 두드러집니다. 실제 부하 환경에서 벤치마킹으로 검증하는 것이 원칙입니다.

Q. 브라우저 클라이언트에서 gRPC를 직접 호출할 수 없나요?
A. 현재 브라우저는 HTTP/2 프레임을 직접 제어하는 API를 제공하지 않아 gRPC를 네이티브로 호출하지 못합니다. gRPC-Web 스펙과 Envoy/grpc-gateway 같은 프록시를 사용하면 브라우저에서 gRPC 서비스를 호출할 수 있습니다. 단, 양방향 스트리밍은 gRPC-Web에서 지원되지 않으며, 단방향 스트리밍까지만 사용 가능합니다.

Q. .proto 파일을 팀 간에 어떻게 관리하면 좋을까요?
A. 전용 proto 리포지토리를 만들어 중앙에서 관리하고, Buf CLI로 lint·breaking change 검사를 자동화하는 방법이 권장됩니다. Buf Schema Registry(BSR)를 활용하면 proto 파일을 패키지로 배포·버전 관리할 수 있어 팀이 많아질수록 유리합니다. CI에 buf breaking --against '.git#branch=main' 검사를 포함해 하위 호환성 깨짐을 사전에 방지하세요.