gRPC 실전gRPC in Practice

gRPC 기본적인 사용 방법The basics of using gRPC

written by tiaz0128
'Protocol Buffers'에 대해 더 자세히 알고 싶으시다면, "gRPC 기초 : Protocol Buffers" 글을 확인 해주세요!

RPC (Remote Procedure Call)

RPC는 클라이언트-서버 모델을 기반으로 원격 프로시저를 로컬 프로시저처럼 호출할 수 있게 해주는 프로토콜입니다. 언어와 상관없이 서버와 클라이언트가 동일한 인터페이스를 기반으로 통신이 가능 합니다.

작동 방식

  1. 클라이언트가 로컬에서 함수를 호출
  2. RPC 시스템이 호출을 직렬화하여 네트워크로 전송
  3. 서버에서 요청을 받아 역직렬화하고 해당 함수 실행
  4. 결과를 다시 직렬화하여 클라이언트로 전송
  5. 클라이언트에서 결과를 받아 역직렬화하여 사용

gRPC

gRPC (gRPC Remote Procedure Calls)

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

gRPC

주요 특징

  • 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를 정의합니다.

protos/person.proto
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 메서드를 포함하고 있습니다.

protos/route_guide.proto
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 전체 코드

src/server.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 전체 코드

src/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 형태로 주고받습니다.

gRPC

여기서는 클라이언트-서버 모두 파이썬으로 컴파일 했지만 동일한 .proto 파일을 각각 다른 언어로 컴파일하고 동일하게 호출 가능합니다.

마이크로서비스 아키텍처(MSA)로 서비스를 구성한다면 서버간의 통신을 gRPC를 이용해 구현해 보는것도 좋은 방법이 될 수 있습니다. 😊

Want to know more about 'Protocol Buffers'? Check out "gRPC Basics: Protocol Buffers"!

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

  1. The client calls a function locally
  2. The RPC system serializes the call and sends it over the network
  3. The server receives the request, deserializes it and runs the function
  4. The result is serialized again and sent back to the client
  5. The client receives the result, deserializes it and uses it

gRPC

gRPC (gRPC Remote Procedure Calls)

gRPC is an open-source RPC framework developed by Google that uses protobuf as its implementation.

gRPC

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.

protos/person.proto
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.

protos/route_guide.proto
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

src/server.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()

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

src/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)

        # 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.

gRPC

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. 😊

gRPC

tiaz0128

Eat Sleep Coding.

Never Never GiveUp.

Security  |  BackEnd  |  Multi Cloud