
1. gRPC란 무엇인가?
gRPC(Google Remote Procedure Call)는 구글에서 개발한 오픈소스 RPC(Remote Procedure Call) 프레임워크로서, HTTP/2 프로토콜을 기반으로 높은 성능, 스트리밍, 양방향 통신 등을 지원합니다. Protobuf(Protocol Buffers)라는 IDL(Interface Definition Language)를 사용하여 직렬화/역직렬화 성능이 뛰어나고, 다양한 언어에서 클라이언트/서버를 작성할 수 있다는 장점이 있습니다.
주요 특징
HTTP/2 기반: 멀티플렉싱 및 스트리밍을 지원하므로, 동시성에 유리하고 양방향 스트리밍 같은 고급 기능을 제공
멀티플렉싱(multiplexing)이란, 하나의 TCP 연결에서 여러 개의 스트림(stream)을 동시에 전송할 수 있게 하는 기능을 말합니다. 전통적인 HTTP/1.1에서는 요청 하나당 하나의 연결(또는 시리얼하게 순차 요청) 방식을 주로 사용했으나, HTTP/2에서는 여러 요청 및 응답을 병렬로 처리 가능합니다.
- 단일 TCP 연결: 하나의 TCP 연결 위에서 여러 스트림을 병렬로 처리하여, 연결 수를 제한하면서도 높은 처리량과 빠른 응답을 얻을 수 있습니다.
- 헤더 압축: HTTP 헤더의 중복 정보를 압축하여 전송 효율을 높입니다.
- 서버 푸시(Server Push): 서버가 클라이언트의 요청 없이도 추가 리소스를 푸시할 수 있습니다(웹 브라우저 환경에서 유리)
- 스트리밍 및 양방향 통신: gRPC에서 클라이언트와 서버가 동시에 스트림을 주고받을 수 있도록 지원합니다.
Protocol Buffers 사용: 가볍고 빠르며 언어에 독립적인 데이터 직렬화 포맷
여러 언어 간 상호운용성: Go, Java, C++, Python, Node.js, C#, Ruby 등 다양한 언어에서 공식 지원
1.1 Protocol Buffers
Protocol Buffers(프로토콜 버퍼)는 구글에서 개발한 언어 중립적, 플랫폼 중립적 데이터 직렬화/역직렬화 방식입니다. .proto라는 IDL(Interface Definition Language) 파일에 메시지 구조를 정의하고, 해당 파일을 컴파일하여 언어별 코드를 생성해 사용합니다.
주요 특징
가볍고 빠른 직렬화/역직렬화: JSON, XML 등 다른 포맷 대비 크기가 작고 처리 속도가 빠릅니다.
언어/플랫폼 독립성: C++, Java, Go, Python 등 다양한 언어를 지원하므로, 서로 다른 환경에서 쉽게 데이터를 교환할 수 있습니다.
버전 관리 용이:
.proto정의에 필드를 추가/삭제해도 과거 클라이언트와 어느 정도 호환되도록 설계되었습니다(규칙에 따라 필드 번호와 타입만 주의하면 하위 호환성 유지 가능).
2. gRPC 구성
2.1 .proto 파일
syntax = "proto3"; // messaage type 정의
package example;
message Person {
string name = 1;
int32 age = 2;
}
코드 생성
protoc --go_out=. --go_opt=paths=source_relative \\ --go-grpc_out=. --go-grpc_opt=paths=source_relative \\ sample/helloworld.protoprotoc: 프로토콜 버퍼(Protocol Buffers) 컴파일러--go_out=.: .proto 파일을 Go 언어용 코드로 생성(*.pb.go파일) 할 때, 출력 위치 지정--go_opt=paths=source_relative: Go 코드 생성 시, 생성된.pb.go파일의 경로 구조를 어떻게 설정할지 지정하는 옵션--go-grpc_out=.: gRPC 서비스 스텁(.pb.go안에 gRPC 관련 인터페이스, 클라이언트/서버 구조체 등)을 Go 언어로 생성 할 때의 출력 위치--go-grpc_opt=paths=source_relative: gRPC 코드 생성 시에도, 생성된 파일의 경로 구조를 어떻게 설정할지 지정하는 옵션
2.2 _pb.go 파일
Protobuf 메시지 정의 및 직렬화 관련 코드를 포함합니다.
.proto파일에서 정의한 메시지(message) 구조체를 생성합니다.Protobuf 메시지를 직렬화(Serialize) 및 역직렬화(Deserialize)가 가능하도록 합니다.
proto.Marshal()및proto.Unmarshal()같은 함수를 통해 바이너리 데이터를 변환할 수 있도록 지원합니다..proto파일
syntax = "proto3"; package example; message Point { int32 latitude = 1; int32 longitude = 2; }point_pb.go파일Protobuf 메시지 자체를 정의하며, gRPC 서비스 관련 코드는 포함되지 않습니다.
package example import ( "google.golang.org/protobuf/proto" ) type Point struct { Latitude int32 `protobuf:"varint,1,opt,name=latitude,proto3" json:"latitude,omitempty"` Longitude int32 `protobuf:"varint,2,opt,name=longitude,proto3" json:"longitude,omitempty"` } func (m *Point) Reset() { *m = Point{} } func (m *Point) String() string { return proto.CompactTextString(m) } func (*Point) ProtoMessage() {}
2.3 _grpc.pb.go 파일
gRPC 서비스 관련 코드를 포함합니다.
.proto파일에서 정의한 서비스(service) 인터페이스를 생성합니다.gRPC 클라이언트 및 서버 인터페이스를 자동으로 생성하여 RPC 호출을 처리할 수 있도록 합니다.
.proto파일
syntax = "proto3"; package example; service MyService { rpc GetPoint (Point) returns (Point); } message Point { int32 latitude = 1; int32 longitude = 2; }point_grpc.pb.go파일
package example import ( "context" "google.golang.org/grpc" ) // MyServiceClient는 클라이언트 인터페이스 type MyServiceClient interface { GetPoint(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Point, error) } // MyServiceServer는 서버 인터페이스 type MyServiceServer interface { GetPoint(context.Context, *Point) (*Point, error) } // UnimplementedMyServiceServer는 기본 구현 type UnimplementedMyServiceServer struct{} func (UnimplementedMyServiceServer) GetPoint(context.Context, *Point) (*Point, error) { return nil, status.Errorf(codes.Unimplemented, "method GetPoint not implemented") } // MyServiceHandler 등록 함수 func RegisterMyServiceServer(s *grpc.Server, srv MyServiceServer) { s.RegisterService(&_MyService_serviceDesc, srv) }
2.4 stub(스텁)
Stub(스텁)은 클라이언트와 서버가 서로 통신할 때, RPC(Remote Procedure Call) 요청을 보낼 수 있도록 생성된 코드를 의미합니다.
클라이언트 Stub: 클라이언트가 gRPC 서버에 요청을 보낼 때 사용하는 코드
서버 Stub: gRPC 서버에서 클라이언트의 요청을 처리할 때 사용하는 코드
gRPC에서는 .proto 파일을 작성하고 protoc을 실행하면, 자동으로 클라이언트와 서버 Stub이 생성됩니다.
2.4.1 stub의 역할
클라이언트에서 원격 gRPC 함수를 호출하는 역할 클라이언트는 Stub을 통해 마치 로컬 함수를 호출하는 것처럼 gRPC 서버의 메서드를 호출할 수 있습니다.
서버에서 gRPC 요청을 처리하는 역할 서버는 Stub을 통해 클라이언트로부터 받은 요청을 처리하고 응답을 반환합니다.
2.4.2 Unimplemented Structure(미구현 구조체)
gRPC의 Go 코드 생성 시, 서비스 인터페이스와 함께 자동으로 만들어지는 Unimplemented<ServiceName>Server 구조체는 해당 서비스 인터페이스의 메서드를 “미구현(Stub)” 상태로 둔 기본 구현체입니다. 예를 들어 .proto 파일에 service Greeter를 정의했다면, helloworld_grpc.pb.go 안에 UnimplementedGreeterServer와 같은 구조체가 생성됩니다.
사용 목적
호환성(Forward Compatibility) 유지
.proto파일에 정의된 서비스에 새로운 RPC 메서드를 추가하거나 기존 메서드가 변경되었을 때, 이미 배포된 서버 코드가 빌드 에러 없이 동작하도록 하기 위함입니다.만약 새 메서드가 추가되어도,
UnimplementedGreeterServer구조체가 기본적으로 “빈” 메서드를 제공하기 때문에, 기존 서버 구현이 “메서드가 없음” 오류를 일으키지 않도록 해 줍니다.
인터페이스 준수 보조
실제 서버 구현에서
UnimplementedGreeterServer를 임베디드(embedded) 필드로 넣어 두면, 서비스 인터페이스에 요구되는 모든 메서드를 자동으로 포함하게 됩니다.type server struct { helloworld.UnimplementedGreeterServer // 여기에 추가 필드를 넣어서 사용 }server가GreeterServer인터페이스를 만족한다는 점을 Go 컴파일러가 인지하게 되어, 빌드 시점에 누락된 메서드가 있는지를 검증할 수 있습니다.
사용자 정의 구현 덮어쓰기(Override)
UnimplementedGreeterServer가 제공하는 기본 메서드는 항상 에러만 반환하거나 아무 동작도 하지 않는 “빈” 구현입니다. 개발자는 필요한 메서드만server구조체에 직접 구현해서 사용하면 됩니다.func (s *server) SayHello(ctx context.Context, req *helloworld.HelloRequest) (*helloworld.HelloReply, error) { return &helloworld.HelloReply{ Message: "Hello, " + req.GetName(), }, nil }