gRPC 실전
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를 이용해 구현해 보는것도 좋은 방법이 될 수 있습니다. 😊