01-hello-world → 02-streaming → 03-protobuf-bench
(기본 구조) (4가지 RPC) (바이너리 원리)
내 코드 (Go)
│
│ .proto 파일 정의
▼
protoc (컴파일러)
│ .pb.go → 메시지 직렬화/역직렬화 코드
│ _grpc.pb.go → 서버/클라이언트 인터페이스 코드
▼
gRPC 런타임
│
│ Protocol Buffers (바이너리 직렬화)
│ HTTP/2 (전송 프로토콜)
▼
네트워크
서비스 A에서 서비스 B의 함수를 호출하려면?
소켓 직접 쓰는 방법:
// 이걸 매번 써야 한다
conn, _ := net.Dial("tcp", "b:8080")
json.NewEncoder(conn).Encode(map[string]any{"name": "Mando"})
var resp map[string]any
json.NewDecoder(conn).Decode(&resp)RPC 방법:
// 로컬 함수 호출처럼 쓴다
resp, _ := client.SayHello(ctx, &pb.HelloRequest{Name: "Mando"})RPC(Remote Procedure Call)는 네트워크 통신을 함수 호출처럼 감추는 추상화 계층이다. 그 안에서 직렬화, TCP 연결, 에러 처리가 자동으로 일어난다.
RPC의 핵심은 **Stub (프록시)**이다.
클라이언트 코드
│
▼
Client Stub (자동 생성)
- 인자를 바이트로 직렬화
- 네트워크로 전송
- 응답 바이트를 역직렬화해서 반환
│
│ ← 네트워크 →
▼
Server Stub (자동 생성)
- 요청 바이트를 역직렬화
- 실제 함수 호출
- 결과를 바이트로 직렬화해서 반환
│
▼
실제 서버 함수
gRPC에서 이 Stub 코드가 protoc가 자동 생성하는 _grpc.pb.go 파일이다.
| 역할 | 기술 |
|---|---|
| RPC 프레임워크 | gRPC |
| 전송 프로토콜 | HTTP/2 |
| 직렬화 포맷 | Protocol Buffers |
1. 멀티플렉싱
HTTP/1.1: 요청1 → 응답1 → 요청2 → 응답2 (순서대로)
HTTP/2: 요청1 ┐
요청2 ├→ 응답1
요청3 ┘ 응답3
응답2 (동시에, 하나의 TCP 연결)
하나의 TCP 연결에 여러 스트림을 multiplexing → Head-of-Line Blocking 해결
2. 헤더 압축 (HPACK)
HTTP/1.1: Content-Type: application/grpc\r\n (매번 전송, ASCII)
HTTP/2: 인덱스 번호 1바이트 (이미 협상된 헤더는 참조만)
3. 바이너리 프레임
HTTP/1.1: GET /path HTTP/1.1\r\nHeader: value\r\n\r\nbody (텍스트)
HTTP/2: [Length 3B][Type 1B][Flags 1B][StreamID 4B][Payload] (바이너리)
프레임 타입:
DATA(0x0): 실제 요청/응답 바디HEADERS(0x1): HTTP 헤더SETTINGS(0x4): 연결 설정WINDOW_UPDATE(0x8): 흐름 제어
클라이언트 서버
│ │
│──HEADERS─────────────────────▶│ :method = POST
│ (요청 헤더) │ :path = /hello.Greeter/SayHello
│ │ content-type = application/grpc
│ │ grpc-encoding = identity
│ │
│──DATA (+ END_STREAM)─────────▶│ [5바이트 길이 접두어] + [protobuf body]
│ (요청 바디) │
│ │
│◀─HEADERS──────────────────────│ :status = 200
│ (응답 헤더) │ content-type = application/grpc
│ │
│◀─DATA─────────────────────────│ [5바이트 길이 접두어] + [protobuf body]
│ (응답 바디) │
│ │
│◀─HEADERS (Trailers)───────────│ grpc-status = 0
│ (스트림 종료) │ grpc-message = ""
│ │
Length Prefix (5바이트):
[Compressed-Flag 1B][Message-Length 4B][Message]
0 = 미압축 바이트 수
1 = gzip/deflate
JSON:
{"name": "Mando", "age": 30}
→ 28바이트 (키 이름 포함, ASCII)protobuf:
필드 1 (name): 0A 05 4D 61 6E 64 6F → 7바이트
필드 2 (age): 10 1E → 2바이트
→ 합계 9바이트 (키 이름 없음, 바이너리)
각 필드는 (field_number << 3) | wire_type 형태의 태그로 시작한다.
| Wire Type | 값 | 사용처 |
|---|---|---|
| Varint | 0 | int32, int64, bool, enum |
| 64-bit | 1 | fixed64, double |
| Length-delimited | 2 | string, bytes, message |
| 32-bit | 5 | fixed32, float |
정수를 가변 길이로 저장한다. 작은 숫자는 1바이트, 큰 숫자는 여러 바이트.
규칙: 각 바이트의 최상위 비트(MSB) = 1이면 다음 바이트가 더 있다는 신호
숫자 1: 0000 0001 → 0x01 (1바이트)
숫자 300: 1010 1100 0000 0010 → 0xAC 0x02 (2바이트)
↑ MSB=1 ↑ MSB=0
(계속) (끝)
계산: 0xAC 0x02
→ 0xAC = 1010 1100, MSB 제거 → 010 1100
→ 0x02 = 0000 0010, MSB 제거 → 000 0010
→ 이어 붙이기 (Little-Endian): 000 0010 | 010 1100 = 0001 0010 1100 = 300
message HelloRequest {
string name = 1; // 필드 번호 = 1
}name = "Hi" 직렬화:
태그: (1 << 3) | 2 = 0x0A (필드 1, wire type 2 = length-delimited)
길이: 0x02 (2바이트)
값: 0x48 0x69 ("Hi" ASCII)
결과: 0A 02 48 69
실제로 확인하는 방법 (03-protobuf-bench에서 실습):
b, _ := proto.Marshal(&pb.HelloRequest{Name: "Hi"})
fmt.Printf("%X\n", b) // 출력: 0A024869# macOS
brew install protobuf
# 설치 확인
protoc --version # libprotoc 27.xgo install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# PATH에 추가 (없으면)
export PATH="$PATH:$(go env GOPATH)/bin"cd 01-hello-world
cat README.md # 단계별 가이드 확인