gRPC 실전

gRPC 기본적인 사용 방법에 대해서

written by tiaz0128

RPC (Remote Procedure Call)


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

작동 방식

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

gRPC

gRPC (gRPC Remote Procedure Calls)


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

gRPC

주요 특징

작동 방식

.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) {}

GetPeople

rpc GetPeople(stream Name) returns (stream 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)를 정의하는 파일입니다. 데이터 구조를 정의하고 조작하는 데 사용합니다.

*_pb2_grpc.py 파일

gRPC 서비스(service)를 정의하는 파일입니다. gRPC 서버 구현 및 클라이언트에서 서비스 호출에 사용합니다.

*_pb2.pyi 파일

타입 힌팅(Type Hinting) 정보를 제공합니다. IDE나 mypy 같은 타입 체커에서 타입 검사 지원하며, 코드 자동 완성 및 문서화 개선하는 역할을 합니다.

이 세 파일을 통해 개발자는 타입 안정성을 유지하면서 효율적으로 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를 상속받아 서비스 로직을 구현합니다.

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)로 이용하여 messageservice를 정의합니다. 컴파일 된 소스를 이용하여 클라이언트-서버를 구현 합니다.

Stub을 이용하여 클라이언트-서버 간 프로시저를 호출 (RPC = Remote Procedure Call) 할 수 있습니다. 통신 데이터는 protobuf 형태로 주고받습니다.

gRPC

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

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

gRPC

tiaz0128

Eat Sleep Coding.

Never Never Giveup.

💫 Python  |  BackEnd  |  Cloud Architect  |  DevOps