TT Lab
요금제

한국어

시작하기
블로그

블로그

gRPC 가 푸는 문제와 전체 그림: 계약서, 코드 생성, 채널, HTTP/2

상태: 초안 (2026-10-17), 일부 실측. 기준 판 grpc/grpc v1.84.0(문서), grpc/grpc-go v1.83.2(커밋 030ee8becb20). 실측은 2026-10-17 03시대(KST) 작은 클러스터에서 Prometheus 를 읽기만 해서 얻었습니다(질의문은 5절). 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 확인하지 못한 것은 「미확인」으로 표시합니다.

목차

1. 직관: 전화 대신 서식

옆 팀에 일을 부탁할 때 전화로 말하면 빠르지만 오해가 생깁니다. 그래서 회사는 서식을 만듭니다. 칸의 이름과 순서, 어느 칸이 숫자인지가 정해져 있으면 받는 쪽은 서식만 보고 처리할 수 있고, 서식이 조금 바뀌어도(칸 하나 추가) 예전 서식을 든 사람과 계속 일할 수 있습니다.

gRPC 는 이 서식을 .proto 파일로 적고, 서식에서 양쪽 언어의 코드를 자동으로 뽑아냅니다. 부르는 쪽은 client.Hit(ctx, req) 처럼 평범한 함수를 부르고, 그 아래에서 라이브러리가 요청을 바이트로 바꿔 HTTP/2 연결로 보내고 답을 다시 객체로 돌려줍니다. 구글이 사내에서 10년 넘게 쓰던 RPC 기반(Stubby)의 다음 판을 2015년 3월에 오픈소스로 만들기로 한 것이 시작이고, CNCF 에는 2017-02-16 에 인큐베이팅 단계로 들어왔습니다(grpc.io/about, CNCF 프로젝트 페이지).

부품 책임 이 시리즈에서
IDL(.proto) 서비스·메서드·메시지의 계약. 필드 번호가 하위 호환의 열쇠 2편
코드 생성(protoc + 플러그인) 언어별 메시지 타입, 클라이언트 스텁, 서버 인터페이스 1편 4절
채널(ClientConn) 이름 풀이, 부하 분산, 연결 유지, 재시도, deadline 4·5·7편
HTTP/2 전송 호출 하나 = 스트림 하나, 5바이트 메시지 접두, 트레일러의 상태 코드 3편

2. 그림

gRPC 한 번의 호출: .proto 에서 서버 함수까지 rally.proto service·message 정의 protoc·플러그인 언어별 코드 생성 클라이언트 스텁 서버 인터페이스 실행할 때: 클라이언트 프로세스 앱: client.Hit(ctx, req) 스텁: 직렬화 → Invoke 채널 (ClientConn) 이름 풀이(resolver) 부하 분산(LB 정책) 서브채널 1 HTTP/2 연결 서브채널 2 HTTP/2 연결 서버 프로세스 HTTP/2 서버 전송 스트림 하나 = 호출 하나 :path 로 메서드 찾기 역직렬화 → 핸들러 앱 코드 Hit() 응답 반환 굵은 선: TCP 하나 위의 HTTP/2 스트림. 요청은 POST, 응답 끝의 트레일러에 grpc-status 가 실린다. 채널은 오래 살려 두고 여러 호출이 연결을 나눠 쓴다.
그림: .proto 에서 서버 함수까지: 코드 생성과 실행 시 계층

3. 왜 JSON over HTTP/1.1 이 아닌가

REST 와 JSON 으로도 원격 호출은 됩니다. gRPC 가 따로 있는 이유를 계약·바이트·연결·실패 네 가지로 나눠 보면 이렇습니다.

축 흔한 REST/JSON gRPC 대가
계약 OpenAPI 는 선택, 코드와 어긋나기 쉬움 .proto 가 곧 코드의 원천 빌드에 코드 생성 단계가 들어감
바이트 필드 이름을 매번 문자로 보냄 필드 번호 + 이진 값. 2편 예제 21바이트 대 JSON 72바이트 사람이 바로 못 읽음(8편의 도구 필요)
연결 HTTP/1.1 은 연결당 요청 하나씩 HTTP/2 스트림으로 한 연결에 많은 호출, 양방향 스트리밍 L4 부하 분산이 연결 단위라 쏠림(5편)
실패 HTTP 상태와 본문 형식이 서비스마다 다름 17개 상태 코드, deadline 전파, 재시도 정책이 라이브러리에 상태 코드를 제대로 고르는 책임은 앱에 남음(3편)

브라우저의 자바스크립트 API 로는 HTTP/2 프레임과 트레일러를 직접 다룰 수 없어 gRPC 를 그대로 쓰기 어렵습니다. 그래서 gRPC-Web 이나 HTTP/JSON 변환 게이트웨이 같은 다리가 따로 있습니다(이 시리즈에서는 다루지 않음).

4. 코드 생성이 만들어 주는 것

예제 계약서입니다. 시리즈 내내 이 서비스를 씁니다(탁구 랠리 한 번을 기록하는 가상의 서비스).

syntax = "proto3";
package tt.v1;

message Rally { int32 rally_id = 1; string server = 2; repeated int32 hits = 3; bool won = 4; sint32 delta = 5; }
message Score { int32 home = 1; int32 away = 2; }

service Scoreboard {
  rpc Hit(Rally) returns (Score);                 // 단항
  rpc Watch(Rally) returns (stream Score);        // 서버 스트리밍
}

이 계약에서 Hit 의 메서드 경로는 /tt.v1.Scoreboard/Hit 입니다. 3편의 프레임 그림도 이 경로를 씁니다.

생성된 클라이언트 스텁이 하는 일은 놀랄 만큼 적습니다. 직접 protoc 를 돌리지 않고, grpc-go 저장소에 들어 있는 실제 생성 파일 (헬스 체크 서비스)을 보면 메서드 하나가 이렇습니다.

const Health_Check_FullMethodName = "/grpc.health.v1.Health/Check"

func (c *healthClient) Check(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) {
	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
	out := new(HealthCheckResponse)
	err := c.cc.Invoke(ctx, Health_Check_FullMethodName, in, out, cOpts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

출처: grpc/grpc-go v1.83.2 health/grpc_health_v1/health_grpc.pb.go(protoc-gen-go-grpc v1.6.2 가 생성), Apache-2.0

5. 실측: 작은 클러스터에서 gRPC 로 말하는 것들

2026-10-17 03:00~03:04(KST), Prometheus 에 다음 질의를 보냈습니다(쿠버네티스 API 서버의 서비스 프록시로 GET, 읽기만).

count by (__name__, job) ({__name__=~"grpc_.*|.*_grpc_.*|rpc_.*"})
sum by (grpc_service, grpc_method, grpc_type) (grpc_client_started_total{job="apiserver"})
sum by (operation_type) (kubelet_runtime_operations_total)

지표에 남은 gRPC 서비스 이름을 모으면 다음과 같습니다(값은 6편에서 자세히).

누가 → 누구에게 gRPC 서비스(지표 라벨) 근거
kube-apiserver → etcd etcdserverpb.KV·Watch·Lease·Maintenance apiserver 의 grpc_client_* 지표
kubelet → containerd runtime.v1.RuntimeService·ImageService kubelet_runtime_operations_total 의 operation_type 21종
앱 → OTel Collector → Tempo OTLP TraceService/Export Collector 의 transport="grpc", 내보내기 대상 :4317
Tempo 내부 /tempopb.Pusher/PushBytesV2, /grpc.health.v1.Health/Check tempo_request_duration_seconds 의 route 라벨
Argo CD UI·CLI → argocd-server application.ApplicationService 등 114개 메서드 grpc_server_started_total
사이드카 → istiod istio.v1.auth.IstioCertificateService/CreateCertificate istiod 의 grpc_server_*
hubble-relay → 노드마다 Hubble observer.Observer·peer.Peer 릴레이 로그, gRPC 프로브(5·6편)

이름이 etcdserverpb.KV 처럼 proto 패키지.서비스 꼴인 것은 4절의 경로 규칙이 그대로 지표 라벨로 내려왔기 때문입니다. 반대로 kubelet 은 gRPC 공용 지표를 쓰지 않고 list_containers 처럼 자기 이름으로 셉니다. 같은 gRPC 라도 관측 방식은 구현마다 다릅니다.

6. 채널의 수명: 연결은 오래, 호출은 짧게

7. 한계

8. 작은 클러스터와의 연결

클러스터를 매일 만지면서도 gRPC 를 직접 짠 적이 없다면, 사실은 이미 하루에 수억 번 쓰고 있다는 점부터 확인하면 좋습니다. 제어 노드 A 의 etcd 는 기동 이후 Range 를 약 8,333만 번 처리했고, kubelet 8대는 CRI 목록 조회만 합쳐 약 6,900만 번을 불렀습니다(6편). 다음 편은 그 호출 하나하나가 어떤 바이트로 나가는지부터 봅니다.

티티 한마디. 서식만 잘 만들어 두면 상대가 Go 든 C++ 든 상관없어요. 칸 번호만 바꾸지 마세요. 번호가 곧 약속이거든요!

참고

모든 자료는 2026-10-17 에 열람했습니다.

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다