gRPC — HTTP-2 기반 RPC가 REST를 보완하는 이유와 사용법
REST로 충분한데 왜 gRPC를 쓸까요? 마이크로서비스 사이에서 매초 수천 번 호출이 오갈 때, JSON 파싱 비용이 무시할 수 없어지기 때문입니다.
gRPC는 Google이 만든 고성능 RPC 프레임워크입니다. Protocol Buffers로 데이터를 바이너리 직렬화하고 HTTP/2로 전송하여, REST 대비 소규모 페이로드에서 최대 5배 빠른 성능을 보여줍니다. 다만 2025년 기준 채택률은 14%로, REST(93%)와 공존하는 형태입니다.
RPC란
RPC(Remote Procedure Call) 는 원격 서버의 함수를 마치 로컬 함수처럼 호출하는 패러다임입니다.
// REST 방식 — HTTP 중심 사고
POST /users
Content-Type: application/json
{"name": "김철수", "email": "kim@example.com"}
// RPC 방식 — 함수 호출 중심 사고
userService.createUser(name="김철수", email="kim@example.com")
REST는 "리소스를 조작한다"는 관점이고, RPC는 "함수를 호출한다"는 관점입니다. gRPC는 이 RPC 패러다임의 현대적 구현체입니다.
gRPC의 3가지 핵심
1. Protocol Buffers — 바이너리 직렬화
Protocol Buffers(Protobuf)는 Google이 만든 데이터 직렬화 포맷입니다. JSON보다 작고 빠릅니다.
// user.proto — 스키마 정의
syntax = "proto3";
package user;
// 서비스 정의
service UserService {
rpc CreateUser (CreateUserRequest) returns (CreateUserResponse);
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User);
}
// 메시지 정의
message CreateUserRequest {
string name = 1; // 필드 번호 = 1
string email = 2; // 필드 번호 = 2
}
message CreateUserResponse {
int64 id = 1;
string name = 2;
string email = 3;
}
message User {
int64 id = 1;
string name = 2;
string email = 3;
string created_at = 4;
}
.proto 파일에서 protoc 컴파일러로 Java, Go, Python 등의 코드를 자동 생성합니다.
JSON vs Protobuf 크기 비교:
// JSON (약 82바이트)
{"id":1,"name":"김철수","email":"kim@example.com","createdAt":"2026-03-28"}
// Protobuf (약 35바이트) — 바이너리, 사람이 읽을 수 없음
0A 05 EA B9 80... (필드 번호 + 타입 + 값)
필드 번호(1, 2, 3...)는 한번 정하면 바꾸면 안 됩니다. 바이너리 인코딩에서 이 번호가 식별자로 사용되기 때문에, 변경하면 하위 호환성이 깨집니다.
2. HTTP/2 — 멀티플렉싱과 헤더 압축
gRPC는 HTTP/2를 전송 프로토콜로 사용합니다.
- ** 멀티플렉싱 **: 하나의 TCP 연결에서 여러 요청/응답을 동시에 처리 (HTTP/1.1의 HOL Blocking 해결)
- ** 헤더 압축 **: HPACK으로 반복되는 헤더를 압축 (메타데이터 오버헤드 감소)
- ** 바이너리 프레이밍 **: 텍스트가 아닌 바이너리 프레임으로 전송 (파싱 효율)
- ** 서버 푸시 **: 요청 없이도 서버가 데이터를 보낼 수 있음 (스트리밍의 기반)
3. 코드 생성 — 타입 안전한 클라이언트/서버
.proto 파일에서 자동 생성된 코드를 사용하므로:
- 컴파일 시점에 타입 오류를 잡을 수 있습니다
- API 문서가
.proto파일 자체입니다 - 클라이언트와 서버가 같은 스키마를 공유하여 불일치가 없습니다
4가지 통신 패턴
1. Unary RPC (일대일)
가장 기본적인 패턴. 요청 하나에 응답 하나.
rpc GetUser (GetUserRequest) returns (User);
2. Server Streaming RPC (서버 → 클라이언트 스트림)
서버가 여러 응답을 스트림으로 전송. 실시간 피드, 대량 데이터 조회에 적합.
rpc ListUsers (ListUsersRequest) returns (stream User);
3. Client Streaming RPC (클라이언트 → 서버 스트림)
클라이언트가 여러 요청을 보내고 서버가 하나의 응답을 반환. 파일 업로드, 센서 데이터 수집에 적합.
rpc UploadFile (stream FileChunk) returns (UploadResult);
4. Bidirectional Streaming RPC (양방향 스트림)
양쪽 모두 스트림으로 주고받음. 채팅, 실시간 게임에 적합.
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
REST는 Unary만 자연스럽게 지원합니다. 스트리밍이 필요하면 WebSocket이나 SSE를 별도로 구현해야 하지만, gRPC는 프레임워크 레벨에서 4가지 패턴을 모두 지원합니다.
gRPC vs REST 비교
| 항목 | REST | gRPC |
|---|---|---|
| 프로토콜 | HTTP/1.1 ~ 3 | HTTP/2 (필수) |
| 데이터 포맷 | JSON (텍스트) | Protobuf (바이너리) |
| 스키마 | 선택 (OpenAPI) | 필수 (.proto) |
| 코드 생성 | 선택 | 필수 |
| 스트리밍 | 별도 구현 필요 | 네이티브 지원 |
| 브라우저 호환 | 완벽 | 제한적 (프록시 필요) |
| 채택률 (2025) | 93% | 14% |
| 성능 | 기준 | 소규모 페이로드에서 최대 5배 빠름 |
벤치마크 수치 (Kong 2026)
- 1,000 동시 접속 기준 gRPC의 지연 시간이 REST 대비 45% 낮음
- 특히 페이로드가 작을수록(마이크로서비스 간 호출) 차이가 큼
- 대용량 페이로드에서는 차이가 줄어듦
언제 무엇을 쓸 것인가
- REST: 공개 API, 브라우저 직접 호출, 범용 호환성 필요
- gRPC: 내부 서비스 간 통신, 저지연 필요, 스트리밍 필요
- ** 현대적 패턴 **: 외부 → REST, 내부 → gRPC (공존)
Connect Protocol — 브라우저에서 직접 gRPC
기존 gRPC의 가장 큰 한계는 브라우저 호환성이었습니다. HTTP/2의 바이너리 프레이밍을 브라우저가 직접 다룰 수 없어서, gRPC-Web + Envoy 프록시가 필요했습니다.
Connect Protocol(Buf 사)은 이 문제를 해결합니다:
- 표준 HTTP를 사용하여 프록시 없이 브라우저에서 직접 호출 가능
- gRPC, gRPC-Web, Connect 세 가지 프로토콜을 하나의 서버에서 동시 지원
- JSON과 Protobuf 인코딩을 모두 지원
// 기존: 브라우저 → Envoy 프록시 → gRPC 서버
// Connect: 브라우저 → Connect 서버 (프록시 불필요)
Spring Boot에서 gRPC 연동
grpc-spring-boot-starter를 사용하면 Spring Boot에 gRPC를 통합할 수 있습니다.
서버 구현
@GrpcService
public class UserGrpcService extends UserServiceGrpc.UserServiceImplBase {
private final UserService userService;
public UserGrpcService(UserService userService) {
this.userService = userService;
}
@Override
public void getUser(GetUserRequest request,
StreamObserver<User> responseObserver) {
// 비즈니스 로직 호출
var user = userService.findById(request.getId());
// Protobuf 메시지로 변환하여 응답
var response = User.newBuilder()
.setId(user.getId())
.setName(user.getName())
.setEmail(user.getEmail())
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
클라이언트 호출
@Service
public class UserClient {
@GrpcClient("user-service")
private UserServiceGrpc.UserServiceBlockingStub userStub;
public User getUser(long id) {
var request = GetUserRequest.newBuilder()
.setId(id)
.build();
// 로컬 함수처럼 호출
return userStub.getUser(request);
}
}
공부하다 보니, gRPC는 설정과 빌드 파이프라인이 REST보다 복잡하지만, 한번 세팅하고 나면 타입 안전성과 코드 생성의 편리함이 꽤 큽니다. .proto 파일 하나 수정하면 클라이언트와 서버 코드가 동시에 업데이트되니까요.
HTTP/3과 gRPC의 미래
HTTP/3(QUIC)는 2025년 10월 기준 35% 채택률에 도달했습니다. gRPC는 현재 HTTP/2를 필수로 사용하지만, HTTP/3 지원이 진행 중입니다. QUIC의 0-RTT 연결과 스트림 독립성은 gRPC의 스트리밍 패턴과 자연스럽게 어울립니다.
정리
- gRPC = Protocol Buffers(바이너리) + HTTP/2(멀티플렉싱) + 코드 생성(타입 안전)
- 4가지 통신 패턴: Unary, Server Streaming, Client Streaming, Bidirectional
- REST는 공개 API, gRPC는 내부 서비스 간 통신 — 공존이 현실적인 선택
- Connect Protocol로 브라우저에서도 프록시 없이 gRPC 호출이 가능해지고 있음
.proto파일의 필드 번호는 절대 변경하면 안 됨 — 하위 호환성의 핵심