gRPC 실전gRPC in Practice
gRPC 기본적인 사용 방법The basics of using gRPC
RPC (Remote Procedure Call)
RPC는 클라이언트-서버 모델을 기반으로 원격 프로시저를 로컬 프로시저처럼 호출할 수 있게 해주는 프로토콜입니다. 언어와 상관없이 서버와 클라이언트가 동일한 인터페이스를 기반으로 통신이 가능 합니다.
작동 방식
- 클라이언트가 로컬에서 함수를 호출
- RPC 시스템이 호출을 직렬화하여 네트워크로 전송
- 서버에서 요청을 받아 역직렬화하고 해당 함수 실행
- 결과를 다시 직렬화하여 클라이언트로 전송
- 클라이언트에서 결과를 받아 역직렬화하여 사용

gRPC (gRPC Remote Procedure Calls)
gRPC는 Google에서 개발한 오픈소스 RPC 프레임워크로, protobuf를 구현체로 사용합니다.

주요 특징
- protobuf를
IDL(Interface Definition Language)로 사용 - HTTP/2 기반의 통신
- 다양한 프로그래밍 언어 지원
작동 방식
- protobuf로 서비스 정의
- 서비스 정의(service)를 기반으로 서버와 클라이언트 코드 생성
- 생성한 코드를 기반으로 클라이언트-서버 통신
.proto 작성
가장 먼저 서버와 클라이언트가 공통으로 사용할 IDL(Interface Definition Language)로 protobuf 파일인 .proto 파일을 작성하겠습니다. protos 폴더 아래 두개의 파일을 작성하겠습니다.
project/
└── protos/
└── person.proto
└── route_guide.proto
person.proto
데이터를 표현하는 message를 정의합니다.
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
enum Gender {
UNKNOWN = 0;
MALE = 1;
FEMALE = 2;
}
Gender gender = 3;
repeated string hobbies = 4;
message Address {
string street = 1;
string city = 2;
}
Address address = 5;
}
route_guide.proto
서버와 클라이언트를 정의하는 gRPC service를 정의합니다. 이 서비스는 두 개의 RPC 메서드를 포함하고 있습니다.
syntax = "proto3";
import "person.proto";
message Name {
string name = 1;
}
service RouteGuide {
rpc GetPerson(Name) returns (Person) {}
rpc GetPeople(stream Name) returns (stream Person) {}
}
GetPerson
rpc GetPerson(Name) returns (Person) {}
- 클라이언트가 서버에 단일 Name 메시지를 보내고, 서버는 단일 Person 메시지로 응답
GetPeople
rpc GetPeople(stream Name) returns (stream Person) {}
- 클라이언트가 Name 메시지의 스트림을 서버로 보내고
- 서버는 Person 메시지의 스트림으로 응답
- 여러 사람의 정보를 한 번에 조회하거나, 실시간으로 계속해서 사람들의 정보를 요청하고 받을 수 있음
컴파일
src 폴더에서 아래의 컴파일 명령어를 실행합니다. protos/ 아래에 있는 .proto 파일들을 대상으로 컴파일 합니다. 컴파일 결과 파일은 src 폴더에 생성 됩니다.
project/
├── protos/
│ └── person.proto
│ └── route_guide.proto
└── src/
└── (현재 위치)
$ python -m grpc_tools.protoc -I=../protos --python_out=. --pyi_out=. --grpc_python_out=. ../protos/*.proto
생성 파일
컴파일 결과 각 .proto 파일에 대해 일반적으로 3개의 Python 파일이 생성됩니다. 각 파일의 기능을 알아봅시다.
project/
├── protos/
│ └── person.proto
│ └── route_guide.proto
└── src/
└── person_pb2_grpc.py (현재 위치)
└── person_pb2.py
└── person_pb2.pyi
└── route_guide_pb2_grpc.py
└── route_guide_pb2.py
└── route_guide_pb2.pyi
컴파일 결과 파일
*_pb2.py 파일
protobuf 메시지(message)를 정의하는 파일입니다. 데이터 구조를 정의하고 조작하는 데 사용합니다.
- .proto 파일에서 정의한 모든 메시지 타입에 대한 Python 클래스
- 각 필드에 대한 getter와 setter 메서드
- 직렬화(serialization)와 역직렬화(deserialization) 메서드
*_pb2_grpc.py 파일
gRPC 서비스(service)를 정의하는 파일입니다. gRPC 서버 구현 및 클라이언트에서 서비스 호출에 사용합니다.
- 서버 측: 서비스 구현을 위한 Servicer 클래스
- 클라이언트 측: 서비스 호출을 위한 Stub 클래스
- 서비스를 gRPC 서버에 등록하기 위한 함수
*_pb2.pyi 파일
타입 힌팅(Type Hinting) 정보를 제공합니다. IDE나 mypy 같은 타입 체커에서 타입 검사 지원하며, 코드 자동 완성 및 문서화 개선하는 역할을 합니다.
- *_pb2.py에 정의된 클래스와 함수의 타입 정보
- 메시지 필드의 타입, 메서드의 파라미터 및 반환 등
이 세 파일을 통해 개발자는 타입 안정성을 유지하면서 효율적으로 protobuf 메시지를 다루고 gRPC 서비스를 구현할 수 있습니다. *_pb2.pyi 파일은 선택적이지만, 대규모 프로젝트나 타입 안정성이 중요한 경우에 매우 유용합니다.
server 구현
파일 생성
project/
├── protos/
│ └── person.proto
│ └── route_guide.proto
└── src/
└── *server.py (추가 파일)
└── ...
sever.py 전체 코드
import grpc
from concurrent import futures
import person_pb2
import route_guide_pb2_grpc
class RouteGuideServicer(route_guide_pb2_grpc.RouteGuideServicer):
def __init__(self):
self.people = {
"Alice": person_pb2.Person(
name="Alice",
age=30,
gender=person_pb2.Person.FEMALE,
hobbies=["reading", "swimming"],
address=person_pb2.Person.Address(
street="123 Main St", city="New York"
),
),
"Bob": person_pb2.Person(
name="Bob",
age=25,
gender=person_pb2.Person.MALE,
hobbies=["gaming", "cycling"],
address=person_pb2.Person.Address(
street="456 Elm St", city="Los Angeles"
),
),
}
def GetPerson(self, request, context):
name = request.name
return self.people.get(name, person_pb2.Person(name="Not Found"))
def GetPeople(self, request_iterator, context):
for name_request in request_iterator:
person = self.people.get(name_request.name)
if person:
yield person
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
route_guide_pb2_grpc.add_RouteGuideServicer_to_server(RouteGuideServicer(), server)
server.add_insecure_port("[::]:50051")
print("Server started on port 50051")
server.start()
server.wait_for_termination()
if __name__ == "__main__":
serve()
필요한 모듈 가져오기
gRPC 라이브러리와 concurrent.futures를 가져옵니다. 프로토콜 버퍼로 생성된 person_pb2, route_guide_pb2_grpc 모듈을 가져옵니다.
import grpc
from concurrent import futures
import person_pb2
import route_guide_pb2_grpc
RouteGuideServicer 클래스
init 메서드에서 샘플 데이터를 만듭니다. route_guide_pb2_grpc.RouteGuideServicer를 상속받아 서비스 로직을 구현합니다.
- GetPerson : 단일 요청-응답 패턴. 한 사람의 정보를 요청하는 데 사용
- GetPeople : 스트리밍 요청-응답 패턴. 여러 사람의 정보를 요청하는 데 사용
class RouteGuideServicer(route_guide_pb2_grpc.RouteGuideServicer):
def __init__(self):
self.people = {
"Alice": person_pb2.Person(...),
"Bob": person_pb2.Person(...),
}
def GetPerson(self, request, context):
name = request.name
return self.people.get(name, person_pb2.Person(name="Not Found"))
def GetPeople(self, request_iterator, context):
for name_request in request_iterator:
person = self.people.get(name_request.name)
if person:
yield person
serve 함수
gRPC 서버를 설정하고 시작합니다. ThreadPoolExecutor를 사용하여 최대 10개의 워커로 동시 요청을 처리합니다. 서버를 50051 포트에 바인딩하고 시작합니다.
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
route_guide_pb2_grpc.add_RouteGuideServicer_to_server(RouteGuideServicer(), server)
server.add_insecure_port("[::]:50051")
print("Server started on port 50051")
server.start()
server.wait_for_termination()
서버 구동
$ python server.py
client 구현
파일 생성
project/
├── protos/
│ └── person.proto
│ └── route_guide.proto
└── src/
└── *client.py (추가 파일)
└── server.py
└── ...
client.py 전체 코드
import grpc
import route_guide_pb2
import route_guide_pb2_grpc
def run():
with grpc.insecure_channel('localhost:50051') as channel:
stub = route_guide_pb2_grpc.RouteGuideStub(channel)
# GetPerson 호출
name = route_guide_pb2.Name(name="Alice")
person = stub.GetPerson(name)
print(f"GetPerson: {person.name}, Age: {person.age}, Gender: {person.gender}, "
f"Hobbies: {person.hobbies}, Address: {person.address.street}, {person.address.city}")
# GetPeople 호출
names = [route_guide_pb2.Name(name="Alice"), route_guide_pb2.Name(name="Bob"), route_guide_pb2.Name(name="Charlie")]
people = stub.GetPeople(iter(names))
for person in people:
print(f"GetPeople: {person.name}, Age: {person.age}, Gender: {person.gender}, "
f"Hobbies: {person.hobbies}, Address: {person.address.street}, {person.address.city}")
if __name__ == '__main__':
run()
필요한 모듈 가져오기
grpc 라이브러리와 프로토콜 버퍼로 생성된 route_guide_pb2, route_guide_pb2_grpc 모듈을 가져옵니다.
import grpc
import route_guide_pb2
import route_guide_pb2_grpc
gRPC 채널 생성
서버와 연결하는 channel를 생성합니다.
with grpc.insecure_channel('localhost:50051') as channel:
스텁(Stub) 생성
서버 서비스를 호출할 수 있는 스텁을 생성합니다. 이 스텁을 이용하여 어려운 구현없이 서버와 통신이 가능합니다.
stub = route_guide_pb2_grpc.RouteGuideStub(channel)
GetPerson RPC 호출
GetPerson을 호출합니다. 반환된 Person 객체의 정보를 출력합니다.
name = route_guide_pb2.Name(name="Alice")
person = stub.GetPerson(name)
print(f"GetPerson: {person.name}, ...")
GetPeople RPC 호출
이름 목록으로 스트리밍 형태로 GetPeople을 호출합니다. 응답도 스트리밍 형태로 받습니다. 각 Person 객체의 정보를 출력합니다.
names = [route_guide_pb2.Name(name="Alice"), ...]
people = stub.GetPeople(iter(names))
for person in people:
print(f"GetPeople: {person.name}, ...")
클라이언트 구동
$ python client.py
GetPerson: Alice, Age: 30, Gender: 2, Hobbies: ['reading', 'swimming'], ...
GetPeople: Alice, Age: 30, Gender: 2, Hobbies: ['reading', 'swimming'], ...
GetPeople: Bob, Age: 25, Gender: 1, Hobbies: ['gaming', 'cycling'], ...
마무리
gRPC는 protobuf를 IDL(Interface Definition Language)로 이용하여 message와 service를 정의합니다. 컴파일 된 소스를 이용하여 클라이언트-서버를 구현 합니다.
Stub을 이용하여 클라이언트-서버 간 프로시저를 호출 (RPC = Remote Procedure Call) 할 수 있습니다. 통신 데이터는 protobuf 형태로 주고받습니다.

여기서는 클라이언트-서버 모두 파이썬으로 컴파일 했지만 동일한 .proto 파일을 각각 다른 언어로 컴파일하고 동일하게 호출 가능합니다.
마이크로서비스 아키텍처(MSA)로 서비스를 구성한다면 서버간의 통신을 gRPC를 이용해 구현해 보는것도 좋은 방법이 될 수 있습니다. 😊
RPC (Remote Procedure Call)
RPC is a protocol, based on the client-server model, that lets you call a remote procedure as if it were a local one. Regardless of language, the server and client can communicate based on the same interface.
How It Works
- The client calls a function locally
- The RPC system serializes the call and sends it over the network
- The server receives the request, deserializes it and runs the function
- The result is serialized again and sent back to the client
- The client receives the result, deserializes it and uses it

gRPC (gRPC Remote Procedure Calls)
gRPC is an open-source RPC framework developed by Google that uses protobuf as its implementation.

Key Features
- Uses protobuf as its
IDL (Interface Definition Language) - Communication over HTTP/2
- Supports many programming languages
How It Works
- Define the service with protobuf
- Generate server and client code from the service definition (service)
- Client and server communicate using the generated code
Writing the .proto Files
First, we’ll write the .proto protobuf files as the IDL (Interface Definition Language) shared by the server and client. We’ll create two files under the protos folder.
project/
└── protos/
└── person.proto
└── route_guide.proto
person.proto
Defines the message that represents our data.
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
enum Gender {
UNKNOWN = 0;
MALE = 1;
FEMALE = 2;
}
Gender gender = 3;
repeated string hobbies = 4;
message Address {
string street = 1;
string city = 2;
}
Address address = 5;
}
route_guide.proto
Defines the gRPC service that describes the server and client. This service contains two RPC methods.
syntax = "proto3";
import "person.proto";
message Name {
string name = 1;
}
service RouteGuide {
rpc GetPerson(Name) returns (Person) {}
rpc GetPeople(stream Name) returns (stream Person) {}
}
GetPerson
rpc GetPerson(Name) returns (Person) {}
- The client sends a single Name message to the server, and the server responds with a single Person message
GetPeople
rpc GetPeople(stream Name) returns (stream Person) {}
- The client sends a stream of Name messages to the server
- The server responds with a stream of Person messages
- Lets you look up several people at once, or keep requesting and receiving people’s information in real time
Compiling
Run the compile command below from the src folder. It compiles the .proto files under protos/, and the output files are generated in the src folder.
project/
├── protos/
│ └── person.proto
│ └── route_guide.proto
└── src/
└── (you are here)
$ python -m grpc_tools.protoc -I=../protos --python_out=. --pyi_out=. --grpc_python_out=. ../protos/*.proto
Generated Files
Compiling typically produces three Python files for each .proto file. Let’s look at what each one does.
project/
├── protos/
│ └── person.proto
│ └── route_guide.proto
└── src/
└── person_pb2_grpc.py (you are here)
└── person_pb2.py
└── person_pb2.pyi
└── route_guide_pb2_grpc.py
└── route_guide_pb2.py
└── route_guide_pb2.pyi
The Compiled Output Files
*_pb2.py Files
These define the protobuf messages. You use them to define and manipulate data structures.
- A Python class for every message type defined in the .proto file
- Getter and setter methods for each field
- Serialization and deserialization methods
*_pb2_grpc.py Files
These define the gRPC service. You use them to implement the gRPC server and to call the service from the client.
- Server side: a Servicer class for implementing the service
- Client side: a Stub class for calling the service
- A function for registering the service with a gRPC server
*_pb2.pyi Files
These provide type hinting information. They support type checking in IDEs and type checkers like mypy, and improve code completion and documentation.
- Type information for the classes and functions defined in *_pb2.py
- Message field types, method parameters and return types, etc.
With these three files, developers can work with protobuf messages and implement gRPC services efficiently while keeping type safety. The *_pb2.pyi file is optional, but very useful in large projects or where type safety matters.
Implementing the Server
Creating the File
project/
├── protos/
│ └── person.proto
│ └── route_guide.proto
└── src/
└── *server.py (new file)
└── ...
Full sever.py Code
import grpc
from concurrent import futures
import person_pb2
import route_guide_pb2_grpc
class RouteGuideServicer(route_guide_pb2_grpc.RouteGuideServicer):
def __init__(self):
self.people = {
"Alice": person_pb2.Person(
name="Alice",
age=30,
gender=person_pb2.Person.FEMALE,
hobbies=["reading", "swimming"],
address=person_pb2.Person.Address(
street="123 Main St", city="New York"
),
),
"Bob": person_pb2.Person(
name="Bob",
age=25,
gender=person_pb2.Person.MALE,
hobbies=["gaming", "cycling"],
address=person_pb2.Person.Address(
street="456 Elm St", city="Los Angeles"
),
),
}
def GetPerson(self, request, context):
name = request.name
return self.people.get(name, person_pb2.Person(name="Not Found"))
def GetPeople(self, request_iterator, context):
for name_request in request_iterator:
person = self.people.get(name_request.name)
if person:
yield person
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
route_guide_pb2_grpc.add_RouteGuideServicer_to_server(RouteGuideServicer(), server)
server.add_insecure_port("[::]:50051")
print("Server started on port 50051")
server.start()
server.wait_for_termination()
if __name__ == "__main__":
serve()
Importing the Required Modules
We import the gRPC library and concurrent.futures, along with the person_pb2 and route_guide_pb2_grpc modules generated from the protocol buffers.
import grpc
from concurrent import futures
import person_pb2
import route_guide_pb2_grpc
The RouteGuideServicer Class
The init method creates sample data. The class inherits from route_guide_pb2_grpc.RouteGuideServicer and implements the service logic.
- GetPerson : a single request-response pattern. Used to request one person’s information
- GetPeople : a streaming request-response pattern. Used to request several people’s information
class RouteGuideServicer(route_guide_pb2_grpc.RouteGuideServicer):
def __init__(self):
self.people = {
"Alice": person_pb2.Person(...),
"Bob": person_pb2.Person(...),
}
def GetPerson(self, request, context):
name = request.name
return self.people.get(name, person_pb2.Person(name="Not Found"))
def GetPeople(self, request_iterator, context):
for name_request in request_iterator:
person = self.people.get(name_request.name)
if person:
yield person
The serve Function
Sets up and starts the gRPC server. It uses a ThreadPoolExecutor to handle concurrent requests with up to 10 workers. It binds the server to port 50051 and starts it.
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
route_guide_pb2_grpc.add_RouteGuideServicer_to_server(RouteGuideServicer(), server)
server.add_insecure_port("[::]:50051")
print("Server started on port 50051")
server.start()
server.wait_for_termination()
Running the Server
$ python server.py
Implementing the Client
Creating the File
project/
├── protos/
│ └── person.proto
│ └── route_guide.proto
└── src/
└── *client.py (new file)
└── server.py
└── ...
Full client.py Code
import grpc
import route_guide_pb2
import route_guide_pb2_grpc
def run():
with grpc.insecure_channel('localhost:50051') as channel:
stub = route_guide_pb2_grpc.RouteGuideStub(channel)
# Call GetPerson
name = route_guide_pb2.Name(name="Alice")
person = stub.GetPerson(name)
print(f"GetPerson: {person.name}, Age: {person.age}, Gender: {person.gender}, "
f"Hobbies: {person.hobbies}, Address: {person.address.street}, {person.address.city}")
# Call GetPeople
names = [route_guide_pb2.Name(name="Alice"), route_guide_pb2.Name(name="Bob"), route_guide_pb2.Name(name="Charlie")]
people = stub.GetPeople(iter(names))
for person in people:
print(f"GetPeople: {person.name}, Age: {person.age}, Gender: {person.gender}, "
f"Hobbies: {person.hobbies}, Address: {person.address.street}, {person.address.city}")
if __name__ == '__main__':
run()
Importing the Required Modules
We import the grpc library and the route_guide_pb2 and route_guide_pb2_grpc modules generated from the protocol buffers.
import grpc
import route_guide_pb2
import route_guide_pb2_grpc
Creating a gRPC Channel
Create a channel that connects to the server.
with grpc.insecure_channel('localhost:50051') as channel:
Creating a Stub
Create a stub that can call the server’s service. With this stub, you can talk to the server without any complicated implementation.
stub = route_guide_pb2_grpc.RouteGuideStub(channel)
Calling the GetPerson RPC
Call GetPerson and print the information from the returned Person object.
name = route_guide_pb2.Name(name="Alice")
person = stub.GetPerson(name)
print(f"GetPerson: {person.name}, ...")
Calling the GetPeople RPC
Call GetPeople with a list of names as a stream. The response also comes back as a stream. Print the information from each Person object.
names = [route_guide_pb2.Name(name="Alice"), ...]
people = stub.GetPeople(iter(names))
for person in people:
print(f"GetPeople: {person.name}, ...")
Running the Client
$ python client.py
GetPerson: Alice, Age: 30, Gender: 2, Hobbies: ['reading', 'swimming'], ...
GetPeople: Alice, Age: 30, Gender: 2, Hobbies: ['reading', 'swimming'], ...
GetPeople: Bob, Age: 25, Gender: 1, Hobbies: ['gaming', 'cycling'], ...
Wrapping Up
gRPC uses protobuf as its IDL (Interface Definition Language) to define messages and services. You implement the client and server using the compiled sources.
With a Stub, you can call procedures between client and server (RPC = Remote Procedure Call). The data exchanged is in protobuf format.

Here we compiled both client and server to Python, but you can compile the same .proto file to different languages and call it the same way.
If you’re building services with a microservices architecture (MSA), implementing server-to-server communication with gRPC could be a great option. 😊