gRPC란?
gRPC는 모든 환경에서 실행할 수 있는 최신 오픈 소스 고성능 원격 프로시저 호출(RPC) 프레임워크이다.
- RPC : RPC는 Remote Procedure Call(원격 프로시저 호출)의 약자로, 별도의 원격 제어를 위한 코딩 없이 다른 주소 공간에서 함수나 프로시저를 실행할 수 있게 하는 프로세스 간 통신 기술을 말한다.
일반적으로 프로세스는 자신의 주소공간 안에 존재하는 함수만 호출하여 실행이 가능하지만, RPC의 경우 자신과 다른 주소공간에서 동작하는 프로세스의 함수를 실행할 수 있게 해주는데, 이는 네트워크를 통한 메시징을 수행하기 때문이다.
RPC는 다양한 언어와 프레임워크로 개발되는 MSA(Micro Service Architecture) 구조의 서비스를 만들게 될 때 유용하고, 비즈니스 로직을 개발하는데에 집중할 수 있다는 장점을 가진다.
RPC는 Client-Server 간의 커뮤니케이션에 필요한 상세정보는 최대한 감추고(언어와 프레임워크에 구애받지 않음), Client-Server는 각각 일반 메소드를 호출하는 것처럼 원격 프로시저를 호출할 수 있다.
RPC의 메커니즘 (IDL 기반)
- IDL(Interface Definition Language)을 사용하여 서버의 호출 규약을 정의한다.
함수명, 인자, 반환값에 대한 데이터 타입이 저장된 IDL 파일을 'rpcgen' 컴파일러를 이용하여 stub 코드를 자동으로 생성한다. (IDL은 인터페이스 정의 언어로, 한 언어에 국한되지 않는 언어 중립적인 방법으로 인터페이스를 표현하고 이를 구현 언어(Java, Python, C 등)로의 매핑을 지원한다).
- Stub을 클라이언트, 서버 프로그램에 각각 포함하여 빌드한다.
Stub은 RPC의 핵심 개념으로 Parameter 객체를 Message로 Marshalling/Unmarshalling하는 레이어이다. 서버와 클라이언트는 서로 다른 주소 공간을 사용하므로 함수 호출에 사용된 매개 변수를 꼭 변환해줘야 한다. 그렇지 않으면 메모리 매개 변수에 대한 포인터가 다른 데이터를 가르키게 되기 때문이다.
- Client의 Stub은 함수 호출에 사용된 파라미터의 변환(Marshalling) 및 함수 실행 후 서버에서 전달된 결과를 변환함
- Server의 Stub은 Client가 전달한 매개 변수의 역변환(Unmarshalling) 및 함수 실행 결과를 변환함
- 클라이언트가 함수를 호출하고, 서버는 수신된 함수에 대한 처리를 서버 Stub을 통해 완료하고, 결과 값을 리턴한다.
Protocol Buffer
gRPC는 IDL로 Protocol Buffer를 사용한다.
프로토콜 버퍼는 직렬화 데이터 구조로, 어떤 언어나 플랫폼에서 통신 프로토콜을 사용하거나 데이터를 저장 할 때 구조화된 데이터로 변환할 수 있다.
프로토콜 버퍼에서는 proto file 에서 데이터 구조를 정의하고 사용해야 한다.(특정 언어에 종속성이 없는 형태로 데이터 타입을 정의하게 된다)
프로토콜 버퍼 데이터는 아래와 같이 일련의 key-value 쌍을 포함하는 메시지로 구성된다.
service CoinService {
rpc GetCoinTradePrice (GetCoinTradePriceRequest) returns (GetCoinTradePriceResponse);
}
message GetCoinTradePriceRequest {
string codes = 1;
string names = 2;
}
message GetCoinTradePriceResponse {
string codes = 1;
string names = 2;
string tradePrices = 3;
string changeRates = 4;
string totalTradePrices = 5;
}
proto file을 protoc 컴파일러로 컴파일하게 되면 각 언어에 맞는 형태의 데이터 클래스가 생성되고, 데이터에 접근할 수 있게 된다.
토이 프로젝트 실습 (Java Spring & Python)
프로젝트는 간단한 가상화폐 모의 투자 서비스이다.
스프링으로 회원 가입 및 로그인, 코인 시세 조회 및 매수 매도 기능, 수익률 조회 기능을 구현했고,
파이썬으로는 Upbit의 API를 이용하여 코인의 현재가를 가져오는 기능을 구현했다.
(해당 프로젝트에서는 Java Spring이 Client가 되고, Python이 Server가 된다)
스프링 환경 설정
- gradle 파일에서 의존성 추가
plugins {
id 'com.google.protobuf' version '0.8.17'
}
dependencies {
implementation 'io.grpc:grpc-all:1.42.1'
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.11.0"
}
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.42.1'
}
}
generateProtoTasks {
all()*.plugins {
grpc {}
}
}
}
- proto file 생성 (proto file은 main 폴더 안에 proto 폴더 안에 위치해야 한다)
syntax = "proto3";
service CoinService {
rpc GetCoinTradePrice (GetCoinTradePriceRequest) returns (GetCoinTradePriceResponse);
}
message GetCoinTradePriceRequest {
string codes = 1;
string names = 2;
}
message GetCoinTradePriceResponse {
string codes = 1;
string names = 2;
string tradePrices = 3;
string changeRates = 4;
string totalTradePrices = 5;
}
위 proto file에 대해 간단히 설명하면, 스프링에서 파이썬으로 구현된 GetCoinTradePrice 함수를 호출할 때 Upbit에서 코인의 현재가를 불러오기 위한 code 값과 코인의 이름을 파라미터로 보내주고,
파이썬에선 GetCoinTradePrice 의 결과 값인 codes, names, tradePrices, changeRates, totalTradePrices를 반환환다.
proto file을 작성하고 빌드하게 되면 아래와 같이 CoinServiceGrpc.java 클래스와 Coin.java 클래스가 생성되게 된다.
아래 코드는 이를 이용하여 파이썬에서 코인의 현재가를 받아오는 함수(getCoinTradePrice)를 실행하고 그 결과를 파싱하여 리턴하는 메서드이다.
public Integer getTradePrice(String code) {
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
.usePlaintext()
.build();
CoinServiceGrpc.CoinServiceBlockingStub stub = CoinServiceGrpc.newBlockingStub(channel);
GetCoinTradePriceResponse response = stub.getCoinTradePrice(GetCoinTradePriceRequest.newBuilder()
.setCodes(code)
.build());
List<Integer> tradePrices = List.of(response.getTradePrices().replace("[", "").replace("]", "").replaceAll("'", "").split(", "))
.stream().map(p -> Integer.parseInt(p)).collect(Collectors.toList());
channel.shutdown();
return tradePrices.get(0);
}
파이썬 환경 설정
파이썬에서도 위와 동일하게 proto file을 작성하고, 터미널에서 아래의 명령어를 실행한다. (신경써야 할 부분은 coin.proto인데 각자 파일에 맞는 이름으로 설정하면 된다.)
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. coin.proto
해당 명령어를 실행하기 전에 파이썬에서 stub을 생성하기 위해 grpcio-tools 라이브러리를 설치해야 한다.
pip install grpcio-tools
이후 위의 명령어를 실행하게 되면 아래와 같이 데이터 클래스가 생성된다.
위 과정이 끝나면 이를 이용하여 서비스를 작성하면 된다.
import grpc
import coin_pb2_grpc
import coin_pb2
from concurrent import futures
import requests
import json
class CoinService(coin_pb2_grpc.CoinServiceServicer):
def GetCoinTradePrice(self, request, context):
url = "https://api.upbit.com/v1/ticker"
codes = request.codes.replace("[", "").replace("]", "").split(", ")
names = request.names.replace("[", "").replace("]", "").split(", ")
querystring = {"markets":codes}
response = requests.request("GET", url, params=querystring)
res_json = response.json()
tradePrices = list()
changeRates = list()
totalTradePrices = list()
for i in range(len(res_json)):
tradePrices.append(int(res_json[i]["trade_price"]))
changeRates.append(res_json[i]['signed_change_rate'])
totalTradePrices.append(int(res_json[i]['acc_trade_price_24h']))
return coin_pb2.GetCoinTradePriceResponse(codes=f'{codes}', names=f'{names}', tradePrices=f'{tradePrices}', changeRates=f'{changeRates}', totalTradePrices=f'{totalTradePrices}')
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
coin_pb2_grpc.add_CoinServiceServicer_to_server(CoinService(), server)
server.add_insecure_port('[::]:50051')
server.start()
server.wait_for_termination()
if __name__ == '__main__':
serve()
해당 코드에서 GetCoinTradePrice 함수는 Upbit API를 이용하여 받아오고 싶은 데이터(현재가, 전일 대비 등락율, 거래 대금)을 파싱하여 가져오는 코드이고, 이 결과를 proto 파일에 정의된 규격에 맞게 보낸다.
모든 코인의 현재가를 조회하는 기능을 실행 예시
마무리
gRPC를 직접 사용해보면서 꽤나 재밌게 했던 프로젝트였고, HTTP 2를 이용하기 때문에 성능상 이점도 존재하는 것 같다. 기회가 된다면 다음 프로젝트 할 때도 적극적으로 이용해보고 싶다.
https://github.com/dlwjddn123/gRPC-Example