REST/JSON은 공개 API의 사실상 표준이지만, 내부 서비스가 수백만 번 서로를 호출하는 MSA 환경에선 텍스트 파싱과 느슨한 계약이 비용이 됩니다. 이번 장은 gRPC가 왜 등장했는지 — Protocol Buffers·HTTP/2·계약 우선 — 를 파고들고, REST·GraphQL과 언제 무엇을 골라야 하는지 시니어의 판단 기준을 세웁니다.
계약 우선(contract-first)과 필드 번호 규칙으로 하위 호환을 지키는 법을 알아요.
REST vs gRPC vs GraphQL을 상황에 맞게 고르고, gRPC의 약점(브라우저·가독성)을 안다.
🧾
REST의 한계
잘 쓰던 REST가 내부 통신에선 왜 아쉬울까
텍스트 오버헤드와 "문서로만 존재하는 계약"
REST/JSON은 사람이 읽기 쉽고, HTTP 캐시·브라우저와 궁합이 좋아 공개 API의 표준이 됐어요. 하지만 서비스 간 내부 호출이 초당 수만 건씩 오가는 환경에선 두 가지가 걸림돌이 됩니다.
🐢 ① 텍스트(JSON) 직렬화 오버헤드
JSON은 사람이 읽는 텍스트 포맷이에요. 필드 이름이 매 메시지마다 문자열로 반복되고, 숫자도 "12345"처럼 문자로 표현돼 크기가 큽니다. 파싱할 때도 문자열을 스캔·변환해야 해서 CPU와 대역폭을 더 씁니다.
한두 번이면 무시할 만하지만, 마이크로서비스가 내부에서 서로를 수백만 번 호출하면 이 오버헤드가 지연·비용으로 누적돼요.
📄 ② 느슨한 계약(loose contract)
REST의 요청·응답 스키마는 대개 문서(Swagger/OpenAPI)나 관례로만 존재해요. 서버가 필드 이름을 바꾸거나 타입을 바꿔도 컴파일 타임에 걸러지지 않고, 런타임에 클라이언트가 깨집니다. 오타·타입 불일치가 배포 후에야 드러나기 쉬워요.
"계약이 코드로 강제되지 않는다"가 핵심 약점이에요. 팀·언어가 갈라질수록 이 리스크가 커집니다.
🧭
REST가 나쁘다는 게 아니에요. 공개 API·브라우저·캐시 친화성에선 여전히 최고예요. gRPC는 이 약점이 실제 비용이 되는 내부 통신을 겨냥해 나온 보완재이지, REST의 전면 대체재가 아닙니다.
🖼️ 그림으로 보기 — REST/JSON vs gRPC, 무엇이 다를까
⚡
gRPC 핵심
Protocol Buffers + HTTP/2
바이너리 스키마와 멀티플렉싱이 만드는 성능
gRPC는 구글이 만든 고성능 RPC(원격 프로시저 호출) 프레임워크예요. 마치 로컬 메서드를 부르듯 원격 서비스를 호출합니다. 성능의 두 축은 Protocol Buffers와 HTTP/2예요.
📐
Protocol Buffers
.proto 스키마 → 바이너리 직렬화
→
🔀
HTTP/2
멀티플렉싱·스트리밍·헤더 압축
→
🤝
생성 스텁
로컬 호출처럼 원격 서비스 호출
📐 Protocol Buffers — 스키마 기반 바이너리 직렬화.proto 파일에 메시지 스키마를 먼저 정의하고, 컴파일러(protoc)가 각 언어의 코드를 생성해요. 실제 데이터는 필드 이름 대신 필드 번호(태그)로 인코딩되는 바이너리라 JSON보다 훨씬 컴팩트하고 파싱이 빠릅니다.
JSON은 {"name":"Kim","age":30}처럼 키를 매번 싣지만, protobuf는 필드 번호 + 값만 바이트로 실어요. 그래서 크기·속도에서 유리합니다.
syntax = "proto3";
message User {
string name = 1; // 1, 2 는 필드 번호(태그) — 직렬화의 키
int32 age = 2;
}
service UserService {
rpc GetUser (UserRequest) returns (User); // 단항
rpc ListUsers (Query) returns (stream User); // 서버 스트리밍
}
🔀 HTTP/2 — gRPC의 전송 계층
gRPC는 HTTP/2 위에서 동작해요. HTTP/2가 주는 것: ① 멀티플렉싱 — 하나의 TCP 연결에서 여러 요청/응답을 동시에(HTTP/1.1의 head-of-line 블로킹 완화). ② 양방향 스트리밍 — 요청·응답을 스트림으로 주고받아 지속 통신 가능. ③ 헤더 압축(HPACK) — 반복되는 헤더를 압축해 오버헤드↓.
"REST/JSON도 HTTP/2로 돌릴 수 있지 않냐?" 맞아요. 다만 gRPC는 스트리밍·바이너리 프레이밍을 HTTP/2에 딱 맞춰 설계돼 있어 이점을 온전히 씁니다.
호출 방식
모양
예시
단항(Unary)
요청 1 → 응답 1
일반 조회. REST와 가장 비슷
서버 스트리밍
요청 1 → 응답 N(stream)
대용량 목록·실시간 피드 전송
클라이언트 스트리밍
요청 N(stream) → 응답 1
파일 업로드·다건 집계
양방향 스트리밍
요청 N ↔ 응답 N
채팅·실시간 협업·게임
🖼️ 그림으로 보기 — gRPC 4가지 통신 방식
💡
양방향 스트리밍은 gRPC의 킬러 기능이에요. HTTP/1.1 REST로는 흉내 내기 어려운(폴링·웹소켓 별도 구축) 지속적 양방향 통신을 하나의 연결에서 자연스럽게 처리합니다. 4가지 방식 모두 같은 .proto 서비스 정의에서 stream 키워드로 선언돼요.
📜
계약 우선
.proto가 곧 계약이자 소스 오브 트루스
코드 생성 · 강타입 · 다언어
gRPC는 계약 우선(contract-first) 방식을 강제해요. .proto에 서비스·메시지를 먼저 정의하고, 그로부터 서버·클라이언트 코드를 생성합니다. 계약이 "문서"가 아니라 컴파일되는 코드로 존재하는 게 핵심이에요.
📝
.proto 정의
서비스·메시지·필드 번호
→
⚙️
protoc 생성
Java·Go·Python… 스텁 생성
→
✅
강타입 호출
컴파일 타임에 계약 검증
🖼️ 그림으로 보기 — .proto 하나로 서버·클라이언트 스텁 생성
✅ 계약 우선이 주는 것강타입(strong typing) — 필드 이름·타입이 코드로 강제돼, 서버·클라이언트 불일치가 컴파일 타임에 걸립니다. 다언어(polyglot) — 같은 .proto에서 Java·Go·Python·C++ 등 여러 언어의 코드를 동시 생성. 팀별 언어가 달라도 계약은 하나. 단일 진실 원천 — .proto 하나가 문서·검증·코드의 기준이 돼 표류(drift)를 막아요.
REST에도 OpenAPI 기반 코드 생성이 있지만, gRPC는 이 흐름이 프레임워크의 기본값이라는 게 차이예요.
🧭
Java에서의 흐름.protobuf-gradle-plugin 같은 빌드 플러그인이 .proto를 컴파일해 메시지 클래스 + 서비스 스텁(blocking/async)을 생성해요. 서버는 생성된 *ImplBase를 상속해 로직만 채우고, 클라이언트는 생성된 스텁으로 메서드처럼 호출합니다.
🧬
API 진화·호환성
필드 번호를 지키면 스키마가 늙지 않는다
하위 호환의 규칙과 REST 버저닝 비교
API는 계속 진화해요. protobuf의 뛰어난 점은 필드 번호 규칙만 지키면 구버전·신버전 클라이언트가 공존할 수 있다는 거예요. 직렬화가 필드 번호(태그) 기준이기 때문입니다.
규칙
왜
필드 번호는 불변, 한 번 쓴 번호는 재사용 금지
번호가 직렬화의 키. 재사용하면 구버전이 엉뚱한 필드로 해석해 데이터 오염
필드 삭제 시 해당 번호를 reserved로 예약
실수로 재사용되는 걸 컴파일러가 막아줌
새 필드 추가는 하위 호환
구버전은 모르는 필드를 무시, 신버전은 없는 필드에 기본값을 씀
proto3는 required 없음(지양)
required는 진화를 막아요 — 나중에 제거/선택화가 불가해 호환성 폭탄이 됨
message User {
string name = 1;
int32 age = 2;
reserved 3; // 예전에 쓰던 3번을 영구 예약 (재사용 금지)
string email = 4; // 새 필드 추가 → 구버전은 무시, 하위 호환 유지
}
⚠️
"필드 이름을 바꿔도 되나?" 직렬화는 번호 기준이라 이름 변경은 바이너리 호환에는 안전하지만, 생성 코드의 API가 바뀌어 클라이언트 컴파일이 깨질 수 있어요. 반대로 번호를 바꾸거나 재사용하면 바이너리 호환이 즉시 깨집니다 — 이게 훨씬 치명적이에요.
🔀 REST의 버저닝과 비교
REST는 스키마 진화 장치가 약해, 큰 변경 시 버전을 분기해요. URL 버저닝(/v1/users → /v2/users)이나 헤더 버저닝(Accept: application/vnd.api.v2+json)이 대표적이죠.
gRPC는 필드 번호 규칙으로 한 스키마를 오래 진화시키는 쪽, REST는 버전을 나눠 병렬 운영하는 쪽에 가까워요. 물론 gRPC도 큰 파괴적 변경이면 서비스/패키지 버전을 새로 팝니다.
🧭
선택 기준
REST vs gRPC vs GraphQL
"무엇이 최고"가 아니라 "언제 무엇"
기술
강한 곳
주로 쓰는 상황
gRPC
저지연·고성능, 스트리밍, 강타입 계약
내부 서비스 간 통신(MSA), 실시간·양방향
REST
단순·범용, HTTP 캐시, 브라우저 친화
공개 API, 브라우저·모바일 대상
GraphQL
클라이언트가 필요한 필드만 조회, over/under-fetch 해결
클라이언트 주도 조회, 다양한 화면이 한 백엔드 사용
🎯 한 문장 판단gRPC — "내부 서비스끼리 빠르고 강타입으로, 스트리밍까지 필요하다." REST — "외부에 공개하고 브라우저·캐시·범용성이 중요하다." GraphQL — "클라이언트마다 필요한 데이터 모양이 달라 조회를 클라이언트가 주도하고 싶다."
현실에선 섞어 써요. 외부엔 REST/GraphQL 게이트웨이, 내부엔 gRPC가 흔한 조합입니다.
⚠️
gRPC의 약점도 분명해요. ① 브라우저에서 직접 호출이 제한돼요 — 브라우저가 HTTP/2 프레임을 세밀히 제어 못 해서, gRPC-Web(프록시 경유)이 필요합니다. ② 바이너리라 사람이 읽기 어려워요 — curl로 눈으로 디버깅하기 힘들고, grpcurl 같은 별도 도구가 필요해요. ③ HTTP 캐시·표준 인프라와의 궁합은 REST가 낫습니다.
💡
gRPC-Web이 필요한 이유. 표준 gRPC는 HTTP/2 트레일러 등 브라우저가 노출하지 않는 기능에 기대요. 그래서 브라우저 ↔ 서버 사이에 Envoy 같은 프록시를 두고 gRPC-Web ↔ gRPC로 변환합니다. "gRPC는 브라우저에서 안 된다"가 아니라 "중간 변환이 필요하다"가 정확해요.
🧠 이 장 핵심 요약
REST/JSON의 약점은 텍스트 직렬화 오버헤드와 느슨한 계약 — 내부 고빈도 통신에서 비용이 된다.