센로그

7. gRPC 본문

CS/풀스택서비스네트워킹

7. gRPC

seeyoun 2023. 12. 8. 11:41

◆ gRPC 개요

최근 분산 서버 프로그래밍에서 각광받고있는 기술 중 gRPC라는 기술이 있다.

  • 개발자: 구글
    • 지금부터 배울 gRPC, HTTP/2, HTTP/3는 다 구글의 기술이다. 
  • 발표일: 2016년 8월 (4년 전)  
  • 저장소: github.com/grpc/grpc
  • Written in: C#, C++, Dart, Go, Java, Kotlin, Node, Objective-C, PHP, Python, Ruby
  • 라이선스: 아파치 라이선스 2.0  
  • 웹사이트: grpc.io
  • 특징: HTTP/2 기반, 프로토콜 버퍼 및 IDL 사용, 단방향/양방향 스트리밍 지원, 흐름제어/차단/비차단/취소/타임아웃 등 부가 기능 제공  
  • 주용도: Microservice architecture(서버간 통신), Mobile app - Backend communication(앱-백엔드 통신)

 


◆ 함수 호출 방법들

 

함수를 호출하는 방법으로 몇 가지 방법이 있다.

  • 정적 링킹
  • 동적 링킹
  • RPC

 

■ 정적 링킹 (Static Linking)

실행 파일 생성시에 필요한 라이브러리를 포함하여 생성함

 

그림을 보자.

다음은 컴파일 방식, 즉 소스 코드가 기계어로 변환되는 방식의 경우 소스 코드로부터 실행 파일을 만들기까지의 그림이다.

(인터프리터 방식인 경우에는 임포트를 해서 모두 다 끌어안으니까 이런 그림이 아님)

  • 내가 만든 프로그램을 소스 코드 레벨에서 컴파일하면 통상 오브젝트 파일(내가 만든 프로그램의 기계어)이 나온다.
  • 이를 이미 만들어져있는 프로그램의 오브젝트 파일이나 라이브러리와 합치는 과정을 링킹이라고 한다.
  • 이렇게 합쳐지고 이 안에 실행 파일로 만들기위한 소프트웨어가 일부 더 들어가면 실행 파일(exe)이 만들어 짐.


메모리에 문제가 없다면 그냥 이 실행 파일을 실행하면 됨.

 

그런데, 풀어야 할 문제가 남아있다.

실행 파일은 컴퓨터의 피지컬 메모리보다는 작아야 한다.

1.2MB가 된 프로그램은 1MB 램에서 동작할 수 없다는 뜻. 0.2MB를 줄여야 함.

그렇다고 해서 프로그램의 기능을 삭제할 수는 없는 경우!! 사용하는 기법이 동적 링킹(dynamic linking) 이다.

 

 

 동적 링킹 (Dynamic Linking)

반드시 돌아가야 하는 라이브러리만 실행 파일에 넣고, 이외의 것들은 필요할 순간에 동적으로 링킹하는 방식

 

Windows의 경우 확장자 .dll 파일이 동적 링크 라이브러리임.

 

우리가 한글(hwp) 프로그램을 설치했다고 하자.

분명 설치 시에는 수백MB를 설치한다고 했는데, 실제 한글 실행 파일(hwp.exe) 용량을 보니 수십MB밖에 안된다. 뭐지!?

→ 수백MB 설치한 것들을 다 실행 파일에 넣은 게 아니고, 실행 파일에는 꼭 필요한 라이브러리만 포함시켜서 링크한 것.

나머지는 별도의 파일로 존재하다가, 실행 중 필요한 순간에 디스크에서 메모리 올리고, 동적으로 링크해서 쓴다.

 

  • 정적 링킹의 경우 실행 파일 생성시에 필요한 모든 라이브러리를 링크해서 메모리에 올린다.
  • 동적 링킹의 경우 라이브러리가 필요한 순간에 메모리에 올려서 링크한다.
    필요 없어지면 링크를 끊어버리고 메모리 해제함.

 

정적 링킹 방식 대비, 실행 파일 크기가 훨씬 작고, 실행시에도 대적으로 적은 메모리를 차지함

→ 실제 physical 메모리가 작더라도, 많은 기능을 가진 프로그램을 실행할 수 있게 됨!

 

근데 사실 현대 컴퓨터의 메모리 사이즈가 급격히 커지면서, 작은 메모리에 꾸겨넣어야 할 프로그램이 상대적으로 줄어들어서 요즘은 이런 기능 별로 안 씀. 

 


◆ 원격 함수 호출 (RPC: Remotd Procedure Call)

분산 네트워크 환경에서 원격지 컴퓨터의 함수를 호출하는 것

 

함수를 호출하는 또 다른 방법으로, RPC가 있다.

  • 정적 링킹 시 함수 호출: 내가 함수를 호출하면, 같은 실행 파일 안에 있는 함수가 동작을 해서 결과를 줌
  • 동적 링킹 시 함수 호출: 내가 함수를 호출하면, 그때 필요한 모듈이 컴퓨터 메모리 안으로 읽혀 들어와서 작업을 마치고 결과를 받음.
  • RPC: 내가 함수를 호출하면, 그 호출된 함수가 내 컴퓨터에서 실행되지 않고, 통신을 타고 원격지에 있는 컴퓨터에서 실행이 됨. 작업을 마치면 통신을 통해 결과를 돌려받음.

 

이때 콜러는 마치 본인에게 있는 함수를 호출하는 것처럼 호출하고, 콜리 역시 본인의 컴퓨터에 있는 함수가 호출하는 것처럼 느낌.

 

 

<과정>

  • Caller가 함수를 호출한다.  
    • 실제 호출되는 함수의 모양이 Caller에 있기는 있어야 함. 이를 stub이라 부름. stub을 호출하는 것.
  • stub이라는 관문을 통하면 IDL이라는 도구에 의해서 굉장히 복잡한 과정이 자동화 됨.
    • IDL(Interface Definition Language): 컴퓨터와 컴퓨터 사이에 정보를 주고받기 위한 인터페이스를 정의하는 언어.
      실제로 통신이 어떻게 이루어지는지 모르겠고 나는 호출하고 결과를 받겠어! 에 대한 방법론적인 개념.
  • 입력 파라미터와 함수 이름을 RPC 프로토콜을 통해서 전달함
  • 이것을 받으면 실제로 호출해야 될 애(Callee)를 찾아서 야 너 호출됐어 결과 다 내놔 함
  • 처리가 완료되면 결과를 받아서 통신을 통해 다시 돌려줌

 


◆ gRPC vs REST API

여러 측면에 대해 gRPC와 REST API를 비교해보자.

 

  • Contract (지켜야 될 규칙, 규약) 
    • REST: 느슨함. HTTP REST는 그냥 철학임. Create할 때 POST하든 PUT하든 그 회사 나름의 규칙을 따르면 된다.
    • gRPC: 엄격함. 호출하고, 결과를 받아오는 규칙을 반드시 지켜야 함. 입력 파라미터 개수, 타입, 리턴 파라미터 타입 등.. 
  • Protocol
    • REST: HTTP 종류면 다 됨. 주소 값을 쓰는 거니까. 
    • gRPC: 현재 HTTP/2 위에서만 돌아감. 성능상, 보안상의 이유
  • Paload (주고받는 정보)
    • REST: 가장 간단하게는 그냥 URL에다가 값 집어넣고, 결과값은 HTTP 리스폰스 콘텐츠로 가져오면 됨. 가져올 게 많으면 json으로 하면 됨.
    • gRPC: 프로토 버퍼를 통해서 입력 파라미터로 주고 리턴 파라미터로 받음.
  • Streaming
    • REST: 스트리밍 별로 안 씀. 보통 요청-응답 정도로 사용함
    • gRPC: 서버-클라 양방향(bidirectional) 스트리밍 가능
  • Browser support
    • REST: 브라우저가 지원함. REST를 많이 썼던 이유 중 웹 브라우저가 이를 지원한다는 이유도 있었다.
    • gRPC: 현재 gRPC를 지원하는 브라우저는 없음. JS에서 호출하는 게 아직은 안 되기 때문. 웹 보다는 일반적인 어플리케이션 레벨에서 통신시 사용함
  • Security
    • REST: HTTP/1.1기반이므로 보안 지원 안함.
    • gRPC: HTTP/2 쓰므로 보안 지원

 


◆ gRPC Exapmles

앞으로 네 가지 타입의 gRPC 예제를 다룰 것임.

  • Unary RPC example
  • Bidirectional Streaming gRPC example
  • Client Streaming gRPC example
  • Server Streaming gRPC example

하나하나 살펴보자.

 


◆ Unary RPC example

함수를 호출하면서 입력 파라미터를 주고 간단한 결과를 가져오는 예시.

 

네 가지 타입 중 가장 간단한 타입부터 살펴보자.

우리가 함수 호출을 했을 때 파라미터 하나를 주고서, 이를 기반한 결과값을 가져오는 굉장히 간단한 형태이다.

 

여기서 알아야 할 가장 중요한 문장을 보자.

아래 구문은 클라이언트는 호출하고 서버는 호출 당할 그 함수를 위한 문법인데, 이런 것들을 확장자가 .proto 파일에다가 쓸 것임. 프로토콜 버퍼라는 이름에서 따온 것.

 

  • RpcFunc: (RPC를 통해서 호출하거나 호출당할) 함수 이름
  • request: 입력 파라미터
  • response: 리턴 값

.proto 파일에다가, RpcFunc은 rpc로써 request를 받으면 response를 돌려주는 애다~ 라고 쓸 것이다.

이제 실제로 Unary example을 구현하는 과정에 들어갈 것이다.

다음 순서대로 진행될 것.

  • Step 1. 원격 호출 함수 작성
  • Step 2. 프로토콜 버퍼 구성 (proto 파일 작성)
  • Step 3. gRPC 클래스 자동 생성
  • Step 4. gRPC 클라이언트 작성
  • Step 5. gRPC 서버 작성
  • Step 6. gRPC 서버 & 클라이언트 실행 결과 확인
  • Step 7. 개발 파일 확인

 


◇ Step 1. 원격 호출 함수 작성

파이썬 파일 안에 우리가 호출 할(호출 당할) 함수를 정의한다.

 

우리는 hello_grpc.py 라는 파이썬 파일 안에 다음 코드를 작성한다.

 

  • 인풋 파라미터를 받아서 제곱해서 돌려주는 함수.
  • gRPC랑 상관 없이 그냥 우리가 함수 정의하던 대로 쓰면 됨.

 


 Step 2. 프로토콜 버퍼 구성 (proto 파일 작성)

.proto 파일을 작성한다.
프로토 파일에다 내가 하고자 하는 서비스와 주고받을 메시지를 정의한다. 

 

우리는 hello_grpc.proto 파일을 만들 것임.

프로토 파일이란 내가 하고자 하는 서비스 주고받을 메시지를 정의한 파일이다.

 

  • 프로토콜 버퍼 언어로 작성한다. (우리는 버전 3 사용).
    이때 프로토 파일은 파이썬 파 같은 게 아니라, 특정 언어에 종속성이 없는 형태이다. 
  • MyService 안에 아까 봤던 rpc RpcFunc (request) returns (response) 형식이 포함되어 있다.
    • MyFunction(MyNumber) returns (MyNumber) {}
    • MyNumber을 받아서 MyNumber을 돌려주는 함수.
    • {} 내부를 비워둬서 실질적으로 하는 행위는 숨김
  • MyService는 service 형식으로 정의되어 있다. service는 (원격으로) 호출될 기능을 의미함.
    • 원격으로 호출되면 내부에 정의되어 있는 MyFunction을 통해서 입력 파라미터 하나를 받아서 처리한 후 리턴 결과값을 돌려줌
  • MyNumber은 message 형식으로 정의되어 있다. 통신에서 주고받는 것을 통상 메시지 라고 함.
    • 32비트짜리 정수 값이고, 변수 이름은 value로 한다. =1은 디폴트 값.
  •  function들은 서비스가, parameter들은 메시지가 되는 것.
    함수 서비스 부분에, 주고받을 값들은 메시지 부분에 쓴다.



이렇게 프로토 파일을 작성하면 된다!

여기서 프로토 파일을 쓴 건 이런 의미 정도임.

내가 제공해줄 수 있는 서비스는 하난데, MyNumber 받으면 MyNumber 리턴하는 서비스다.
MyNumber는 32비트 정수다.

이렇듯 내가 하고자 하는 서비스 주고받을 메시지에 대해서 정의를 한 것이고, 아직 (Step 1에서 정의한) 실제 MyFunction 하고는 연결이 안된 것이다.

따라서 뒤에서 연결해줘야 함.

 

암튼 이렇게 프로토 파일 작성이 완료되면, 프로토콜 버퍼 컴파일러(protoc)에 넘김.

protoc는 프로토 파일을 입력으로 받아서, 몇가지 결과 파일을 만들어줌. 이 내용을 Step 3에서 다룸.

 


 Step 3. gRPC 클래스 자동 생성

다음 명령어를 통해 protoc는 hello_grpc.proto라는 입력 파라미터를 가지고 소스 코드를 몇 개를 만들어 준다.

 

  • python -m grpc_tools.protoc: 내가 파이썬에서 grpc_tools모듈의 프로토 버퍼 컴파일러를 실행할 거야.
  • -I: 입력 파라미터의 디렉토리를 옵션으로 줄 수 있음.
    만약 이 파일이 현재 디렉토리가 아닌 다른 곳에 있으면 이 뒤에 경로를 쓰면 됨.
    • 우리는 .으로 찍어서 현재 디렉토리로 함.
  • 우리는 프로토 파일을 기반으로 소스 코드를 일단 최소 두 개 만들 것임.
    메시지를 위한 파일과, 서비스를 위한 파일.
    • --python_out=: 메시지 파일 저장 위치
    • --grpc_python_out=: 서버/클라이언트용 서비스 파일 저장 위치
  • 그리고 뒤에 입력 파라미터인 프로토 파일을 넣어주면 됨. 아까 만들었던 hello_grpc.proto를 넣음.

 

그러면 protoc는 다음 두 출력 파일들을 생성한다.


파일의 이름은 자동으로 이렇게 생성된다.

  • 우리가 입력 파라미터로 준 파일 이름이 hello_grpc 였고, 
  • 메시지 파일에는 원본 이름 뒤에 _pb2가, 서비스 파일에는 _pb2_grpc가 붙는 형식.
    • 서비스 파일에는 Servicer/Stub 코드가 있음. 각각 서버용/클라용 코드임

이것은 클라이언트하고 서버를 이렇게 만드세요~ 하는 일종의 예제 파일이다.

안에 들어가면 클래스나 기타 등등 코드가 있다.

 

 

특히 중요한 것은 hello_grpc_pb2_gprcMyService 부분이다.
내가 프로토 파일에서 정의했던 그 MyService임.


그런데 MyService가 왜 Servicer, Stub 이렇게 두 개나 있을까

  • Stub: 더미, 멍텅구리, 관문 역할. 클라이언트용 코드
  • Servicer: 서비스를 제공하는 자. 서버용 코드

클라/서버용으로 나눈 것임 ㅇㅇ

 

 

자 이제 기본적인 세팅은 끝났다.

실제 클라랑 서버 만들어서 실행하면 됨!!

 


 Step 4. gRPC 클라이언트 작성

gRPC 클라이언트 코드를 작성하는 순서는 다음과 같다.

(1) grpc 모듈을 import 함
(2) protoc가 생성한 클래스를 import 함
(3) gRPC 통신 채널을 생성함
(4) protoc가 생성한 _pb2_grpc 화일의 stub 함수를, (3)의 채널을 사용하여 실행하여 stub를 생성함
(5) protoc가 생성한 _pb2 화일의 메세지 타입에 맞춰서, 원격 함수에 전달할 메시지를 만들고, 전달할 값을 저장함
(6) 원격 함수를 stub을 사용하여 호출함
(7) 결과를 활용하는 작업을 수행함 [optioanal]

 

 

코드를 보면서 구체적으로 설명하겠음.

다음은 lec-07-prg-01-hello_gRPC/client.py 코드이다.

(1)  Import grpc 해줌

 

(2)  protoc가 자동으로 생성한 두 개의 파일이 있었다.

  • 메시지에 관한 내용을 담은 hello_grpc_pb2
  • 서비스에 관한 내용을 담은 hello_gprc_pb2_grpc

둘다 import 해준다.

 

(3)  IP주소와 포트 번호를 통해서 insecure_channel()를 호출하는데, 이것은 통신 채널을 연다(통신에 대한 기능을 초기화한다)는 의미임.

  • gRPC는 통신과 관련된 부분을 숨긴다. 따라서 통신과 관련된 작업은 제일 처음에 한 번만 하면 됨.

 

(4)  자동으로 생성된 클라이언트용 스텁 함수를 통해서, 스텁을 생성한다. 입력 파라미터로 전달한 channel을 통신 채널로 사용하도록 한다.

 

(5)  이제 기본 작업들은 다 끝났다.

함수를 호출하려고 하는데, 파라미터로 내가 정의했던 메시지 타입인 MyNumber을 줘야 한다. 

MyNumber는 파이썬의 기본 데이터 타입이 아니므로, 직접 만들어 준다.

  • 자동으로 생성된 메시지 파일을 통해서 request라는 이름으로 MyNumber 객체를 만들고 파라미터로 4 값을 줬다.
    얘를 이제 gRPC를 통해서 입력 파라미터로 보내면 됨.

 

(6)  이제 진짜로 함수 호출하면 됨.

내가 호출해야 하는 함수는 결국, 이 안에 숨고 숨어 있는 MyFunction이다.

(stub안에 hello_grpc_pb2_grpc의 MyServiceStub안에 있는 MyFunction.)

얘를 호출하면 된다. 방금 만든 입력 파라미터(request)를 넘겨주어 실제 원격 함수를 호출한다.

 

(7) 결과를 받아서 화면에 뿌린다.

 


 Step 5. gRPC 서버 작성

이제 gRPC 서버를 만들어보자.

 

gRPC 서버 코드를 작성하는 순서는 다음과 같다.

(1) grpc/futures 모듈을 import 함
(2) protoc가 생성한 클래스를 import 함
(3) 원격 호출될 함수들을 import 함
(4) protoc가 생성한 Servicer 클래스를 base class로 해서 원격  호출될 함수들을 멤버로 갖는 서버 클래스를 생성함
(5) 서버 클래스에 원격 호출될 함수에 대한 rpc 함수를 작성함
(6) grpc.server를 생성함
(7) add_CalculatorServicer_to_server()를 사용해서,  grpc.server에 (4)의 Servicer를 추가함
(8) grpc.server의 통신 포트를 열고, start()로 서버를 실행함
(9) grpc.server가 유지되도록 프로그램 실행을 유지함

 

 

코드를 보면서 구체적으로 설명하겠음.

 

(1)  grpc 및 futures 모듈 임포트

  • futures는 비동기 처리를 위해 사용하는 모듈. request와 response를 비동기적으로 처리해서 full duplex를 구현하기 위함.

(2)  클라이언트와 마찬가지로, protoc가 자동으로 생성한 두 개의 파일을 import 한다.

  • 메시지에 관한 내용을 담은 hello_grpc_pb2
  • 서비스에 관한 내용을 담은 hello_gprc_pb2_grpc

(3)  클라이언트에는 없었지만, 서버에는 실제로 '호출 당할 함수'가 있어야 함.
따라서 실제 작업을 할 hello_grpc를 import함.


(4)  이제 서버를 만들어야 함. 이미 grpc protoc가 서버와 관련된 웬만한 건 다 자동으로 만들었음!
따라서 우리는 그거를 재사용하면 됨.

  • hello_grpc_pb2_grpc파일에 MyServiceServicer가 만들어졌었음. 서비서가 서버.
    • 얘를 베이스 클래스로 삼아서, MyServiceServicer 클래스를 만든다.

 

(5)  이제 실제로 서비스할 RPC 함수를 작성한다.

실제로 실행되어야 될 함수는 hello_grpc.my_func()인데, 얘를 호출하는 규칙을 정의해놓는 것이다.

  • 프로토 파일의 MyService에 정의한 RPC 함수 이름인 MyFunction을 그대로 적어준다.
    • 현재 우린 컨텍스트에 대한 얘기는 하지 않기 때문에, 파라미터의 self, request, context에서 self와 context는 기계적으로 넣는다고 생각하자.
    • 중요한 것은 request임. 이걸 받아서 우리가 작업을 한다. 
  • MyNumber 형태를 돌려줘야 하니 response 객체를 만든다.
  • response 안에  ‘현재 우리가 임포트한, 원래 타겟되는 함수에서의 결과값’ 을 할당한다.
    • hello_grpc.my_function(request.value)를 넣으면, request에 들어가 있는 값을 가져올 수 있다. 
    • 이때 value는 우리가 MyNumber에 멤버로 넣었던 그 value임.

 


(6)  이제 실행하자. gRPC 서버를 생성한다. gRPC에는 grpc.server() 라는 서버 함수가 있다. 이거 그냥 실행하면 됨.

  • futures를 통해서 멀티 스레딩이 되는 gRPC 서버 10개를 만든 것

이때 이 servergrpc 서버임. 아무런 기능이 없는 애다. 얘를 이제 내가 만든 기능인 MyServiceServicer과 연결을 해야 함.

 

(7)  gRPC 서버(server)에 실제로 실행해야 할 서비서(MyServiceServicer())를 연결함.

  • 자동으로 만들어져 있는 클라이언트 서버 모듈 안에 함수 이름이 아예 내 서비서 이름을 가지고 만들어져 있음.
  • add_MyServiceServicer_to_server()

 

(8)  이제 gRPC 서버를 띄운다.

  • 포트 넘버와 IP 주소를 통해 서버 열고 실행시킴.

 

(9)  무한 루프 돌면서 실행하면 됨.


 Step 6. gRPC 서버 & 클라이언트 실행 결과 확인

잘 작동되는지 보자.

 

아까 value에 4를 줬으니, 제곱한 값인 16을 잘 돌려주고 있음

 


 Step 7. 개발 파일 확인

사용된 파일들에 대한 정리이다. naming 및 using에 관한 내용

  • hello_grpc.py : 원격 호출 대상이 정의된 파일  
  • hello_grpc.proto : 프로토버퍼 정의 파일  
  • hello_grpc_pb2.py : (자동생성) 메시지 클래스 파일
  • hello_grpc_pb2_grpc.py : (자동생성) Client/Server 클래스 파일.
    • Servicer 및 Stub 포함
  • server.py : Server 파일 (원격 호출 대상 파일 지원).
  • client.py : Client 파일.

 


◆ Bidirectional Streaming gRPC example

지금부터 우리가 다룰 것은 스트리밍(Streaming)이다.

클라이언트와 서버 사이에 정보를 주고받는데, 끊임없이 주고받는 형태를 의미한다.

 

먼저 양방향 스트리밍을 볼 것이다.

 

기본적으로 양방향 스트리밍을 쓰는 이유는:

클라이언트가 서버로, 서버가 클라이언트로 끊임없이 서로가 양방향으로 정보를 주고받기 위함이다.

 


Unary Bidirectional Streaming의 작동 방식을 비교해보자.

  • Unary방식에서, 클라이언트가 엘리먼트 5개짜리 리스트를 가지고 원격 함수를 호출한다고 하자. 
    • 리스트 엘리먼트 5개를 만들어서 한꺼번에 모아서 서버한테 던짐.
    • 서버가 5개를 한번에 받고 이를 기반으로 처리해서 결과를 만든 다음에, 또 다시 리스트를 돌려준다.
    • 이때 클라이언트는 5개짜리 리스트 보낸 상태에서 기다리고 있다가, 서버가 다 처리하고 응답을 딱 주면 그때 응답을 딱 받는다.
  • Bidirectional Streaming 방식에서는 다르게 처리한다.
    클라이언트가 엘리먼트 5개짜리 리스트를 서버에게 입력 파라미터로 줄 거라고 하자.
    • 한꺼번에 리스트 엘리먼트를 다 만들어서 상대방에게 한꺼번에 모아서 보내는 게 아니라,
      리스트 첫 번째 엘리먼트 한 번 보내고, 리스트 두 번째 엘리먼트 보내고, 리스트 세 번째 엘리먼트 보내고…
      이런 식으로 연속적으로 보낸다. 이런 걸 Streaming이라고 함.

    • 이때 클라이언트가 서버로 보내는 것과, 서버가 클라이언트로 보내는 것이 독립적으로 동작한다.
      구체적인 구현은 프로그래머 마음이다.
      • 클라이언트가 서버로 계속 뭔가 보낼 때, 이와 상관없이 서버도 클라이언트한테 계속 뭔가를 독립적으로 보내면서 주고받을 수도 있다.
      • 상대방이 정보를 다 받을 때까지 한쪽이 기다렸다가, 다 받고 나면 작업을 하도록 구현할 수도 있다.
      • 등등..

 

※ 원래 전통적인 RPC 같은 경우에는 이런 기능이 없었다.
gRPC의 경우 고정되지 않은 리스트, 컬렉션 데이터 타입의 값들을 그때그때 하나씩 만들어가면서 전달할 수 있는기능을 구현할 수 있게 된 것이다!

 

 

Bidirectional Streaming에서는 프로토 파일을 정의하는 방법도 살짝 바뀐다.

  • stream 이라는 구문이 추가적으로 들어갔다. 
  • 입력을 스트리밍으로 주고 출력을 스트리밍으로 받겠다는 뜻이다.

바로 코드를 보면서 적용해보자.

 


◇ 프로토 버퍼 구성

우리가 만들 예제는 앞에서 예시로 언급한 내용과 같다.

클라이언트가 리스트를 서버에게 전달하는 예제이다.

 

이때 완성된 리스트로써 한 번에 통째로 보내는 게 아니라,

어떤 작업을 해서 리스트 엘리먼트를 만들고, 만들어진 엘리먼트 미리 보내고, 보내면서 또 다시 다음 엘리먼트를 만들고 있고, 그걸 또 보내고 ...  이렇게 계속 끊임없이 돌아가는 예제를 구현할 것임.

 

우리의 프로토 파일 bidirectional.proto 은 다음과 같이 구성한다.

프로토 파일에는 호출할/호출당할 함수에 대한 부분이 있고, 주고 받을 메시지에 대한 부분이 있다고 했다.

이때 함수 서비스 부분에, 주고받을 값들은 메시지 부분에 쓴다고 했었다.

  • Bidirectional이라는 service를 만든다.
    • 서비스에는 rpc로 호출할/호출당할 함수인 GetServerResponse() 가 들어가 있다. 
    • 이때 입력 파라미터 부분과 출력 파라미터 부분에 stream이라고 쓰여져 있다.
  • Message라는 이름의 message를 만든다.
    • 하나의 문자열을 멤버로 갖고있다. 이를 Message라는 이름을 통해서 주고받게 될 것임.

 


◇ Client 코드

클라이언트 코드부터 먼저 보자.

  • 메인에서 run() 이 실행된다.
  • run()
    • IP주소와 포트 번호를 사용해 통신 채널을 열어준다. 
      • with-as 구문은, with 절이 제대로 호출되는 경우 이를 as 절에 넣는다는 의미.
        grpc.insecure_channel(‘localhost:50051’)가 제대로 작동하면 이를 channel에 넣는다.
    • protoc가 자동으로 만들어준 bidirectional_pb2_grpc.BidirectionalStub 함수에다 내가 만든 채널을 넣어서 연결한 stub을 만듦. 이 stub을 통해서 서버 쪽으로 정보를 보낼 수 있게 된다.
    • stub을 파라미터로 주어 send_message 함수를 호출한다.
  • send_message(stub)
    • stub의 서비스 내부에 있는 GetServerResponse 함수를 호출할 건데, 이때 파라미터로 generate_message()함수를 호출해서 보내도록 한다.
  • generate_message()
    • 메시지를 리스트로 갖고 있는데, 각각 엘리먼트는 make_message 함수를 통해 만들어졌다.
    • make_message 함수는 파라미터로 받은 파이썬 문자열을 내가 정의한 gRPC 프로토 파일의 message 형태인 bidirectional_pb2.Message 형태로 바꿔주는 역할을 한다.
      • 굳이 이 과정을 넣은 것은, 고정된 엘리먼트들을 리스트화해서 보내는 게 아니라, 무언가 작업을 한 후 결과로 엘리먼트를 만들어서 리스트를 채워가며 보내는 과정을 보여주기 위함이다.
    • return이 아닌 yield를 사용한다.
      • 이는 리스트를 한번에 리턴하는 것이 아닌, 리스트 안에 있는 엘리먼트들을 하나씩 끄집어내서 리턴해주는 것을 의미한다.
      • 이런 방식으로 streaming을 구현하고 있는 것.

 


◇ Server 코드

이번에는 서버 코드를 보자.

  • 메인에서 serve()가 실행된다.
  • serve()
    • grpc 서버를 생성한다. 스레드 10개로 띄운다.
    • protoc가 만들어준 bidirectional_pb2_grpc.add_BidirectionalServicer_to_server() 함수를 통해서, 내가 만든 서비서 BidirectionalService와 grpc 서버를 연결한다.
    • IP주소와 포트 번호를 사용해 grpc 서버를 열고, 실행한다.
  • BiderectionalService 클래스
    • protoc가 만들어준 서버 클래스(bidirectional_pb2_grpc.BidirectionalServicer)를 상속하는 BiderectionalService 클래스를 만든다. 
    • 여기에 실제로 호출되어 동작할 서버의 함수, GetServerResponse 함수를 정의한다.
      • 클라이언트쪽에서 보낸 정보를 request_iterator에서 받아서, 하나씩 yield 해주는 함수이다.

 


◇ Server & Client 실행 화면

실행 결과는 다음과 같다.

서버는 실행이 되면서 본인의 통신 포트를 만들어내고, 서비서에 연결한다.

클라이언트는 통신 채널 연결하고 실행한다.

클라이언트에서 서버 쪽으로 5개가 갔고, 서버에서 클라이언트로 다시 돌려줘서 5개가 왔다.

 

이때 한번에 보내고 한번에 받는 게 아니라, 엘리먼트 단위로 하나씩 작업을 하고 보내고 받고 하면서 Streaming 하고 있다.

 


◆ Client Streaming gRPC example

앞서 클라-서버 간에 bidirectional하게 스트리밍하는 예제를 봤다.

 

클라이언트 스트리밍은 클라이언트가 입력 파라미터로 끊임없이 뭔가를 만들서 상대방에게 전달하는 경우에 사용하는 방식이다.

  • 클라이언트가 서버로 보낼 때 stream 형식으로 보내는 것을 의미한다.
  • 서버는 이를 다 받은 다음에 처리할 수도 있고, 받으면서 처리할 수도 있음. (얘도 실제로 받을 때는 하나씩 받음)
  • 서버에서 클라이언트로 보내는 리턴값은 한번에 돌려줌.

 

bidirectional 방식 조작하면 양쪽 다 스트리밍 하도록 하는 게 아니라 한쪽만 스트리밍 하도록 구현할 수 있다.

 

  • Client Streaming
    • 클라이언트만 스트리밍을 하는 경우.
    • 프로토 파일에서, 클라이언트가 서버 쪽으로 보내는 입력 파라미터부터 부분에 스트림 문법이 들어간다.
  • Server Streaming
    • 서버만 스트리밍을 하는 경우.
    • 프로토 파일에서, 서버가 클라이언트에게 응답을 주는 리턴 부분에 스트림 문법이 들어간다.

 

 


◇ 프로토 버퍼 구성

이번에는 클라이언트가 stream으로 보낸 Message를 서버가 하나하나 받은 후에,

총 몇 개나 보냈는지에 대한 Number을 돌려주는 예제를 작성할 것이다.

 

  • ClientStreaming이라는 service를 만든다.
    • rpc로 호출할/호출당할 함수인 GetServerResponse가 들어있다.
    • 입력 파라미터는 stream으로 보내는 Message이고, 리턴 값은 단일 Number 값이다.
  • Message와 Number라는 message를 각각 만든다.
    • Message는 string으로 구성되며, Number은 int32로 구성된다.

 


◇ Client 코드

아까와 유사한 코드이므로 바뀐 부분을 중심으로 보자.

  • 메인에서 run()이 실행된다.
  • run()
    • IP주소와 포트 넘버로 통신 채널은 만든다.
    • 채널을 stub에 연결한다.
  • send_message(stub)
    •  stub의 서비스 안에 있었던, 궁극적으로 우리가 호출해야 될, 서버에 있는 RPC 메서드인 GetServerResponse를 호출한다. 
    • 파라미터 값으로는 generate_messages()를 통해서 리스트를 만들어서, yield로 하나씩 전달한다.
    • 단, 입력 부분만 스트리밍이고, 결과는 한 번에 가져오게 되어 있다.
      (아까는 for 문을 통해 하나씩 꺼내왔었음. 이번엔 그냥 리스폰스를 가져와서 화면에 출력하고 있다.)

 


◇ Server 코드

서버 코드를 보자.

  • 메인에서 serve() 실행
  • serve()
    • grpc 서버 만들고, grpc 서버와 내가 만든 서비서(ClientStreamingServicer())를 연결하고, 서버 돌림
  • GetServerResponse(self, request_iterator, context)
    • 이전과 동일하게, 스트리밍 되는 메시지를 하나씩 가져오긴 함.
    • 이때 몇 개나 받았는지 count를 하고, (스트리밍 없이) 한번에 클라이언트에게 리턴해줌.

 


◇ Server & Client 실행 화면

실행 결과이다.

 

 

클라이언트가 서버에게 총 5개의 엘리먼트를 보낼 동안 서버가 카운트하고,

서버가 클라이언트에게 리턴값으로 5라고 보낸 것.

 


◆ Server Streaming gRPC example

Client Streaming과 반대되는 방식이 바로 Server Streaming.

 

즉, 클라이언트는 서버에게 한번에 딱 request를 전달하고, 

서버쪽에서 클라이언트에게 줄줄이 response를 내려보내는 형태. (이때 stream 문법이 들어감)

 

 


◇ 프로토 버퍼 구성

이번에 구현할 예제는, 아까와 반대로

클라이언트가 서버에게 숫자(Number)를 주면,

서버가 클라이언트에게 해당 숫자만큼 메시지(Message)를 만들어서 스트리밍해주는 형태이다.

 

  • ServerStreaming이라는 service를 만든다.
    • rpc로 호출할/호출당할 함수인 GetServerResponse가 들어있다.
    • 입력 파라미터는 단일 Number 값이고, 리턴 값은 Message를 스트림으로 받는다.
  • Message와 Number라는 message를 각각 만든다.
    • Message는 string으로 구성되며, Number은 int32로 구성된다.

 


Client 코드

클라이언트 코드를 보자.

  • run()
    • 통신 채널 열고, stub 만들어서 연결한다.
  • recv_message()
    • 이전과 달리 서버의 RPC함수인 GetServerResponse에다가 메시지를 하나씩 꺼내서 스트리밍하지 않고, 그냥 단일 값을 넣어줬다.
    • 대신 서버로부터 받을 때 줄줄이 받으므로, for문을 사용해 하나씩 끄집어내서 보여주고 있다.

 


Server 코드

서버 코드를 보자.

  • GetServerResponse(self, request, context)
    • 클라이언트로 부터 받은 숫자가 얼마인지 출력한다. 
    • make_message 함수를 호출해서 리스트를 만들고,  yield 문법을 통해 하나하나 보낸다.

 


Server & Client 실행 화면

서버는 클라이언트로부터 받은 숫자를 출력하고, 그 숫자만큼 메시지를 하나씩 내려보내는 것을 볼 수 있다.

 


◆ Protobuf 개요

그러면 도대체 프로토 버퍼가 뭔지, 기술적 측면에서 살펴보자.

  • Protobuf가 gRPC가 나오면서 새롭게 등장한 것은 아니다. 
    한 곳에서 다른 곳으로 정보를 끊임없이 스트리밍하는 처리를 하기 위해 2001년에 구글이 만든 SW임.
  • 여러 언어로 작성되었다.
    : C++, C#, Java, Python, JavaScript, Ruby,  Go, PHP, Dart
  • 종류: Serialization format and library, IDL compiler
    • (네트워크 입장에서 봤을 때) 언어나 OS, 하드웨어 플랫폼 따위에 독립적으로 통신할 수 있게 데이터를 직렬화하는 형태나 라이브러리.
    • IDL로 정의된 구조화된 데이터를 직렬화하는 코드를 다양한 프로그래밍 언어로 생성하는 컴파일러
※ Serialization 이란?
하나의 어플리케이션을 만들 때, 프론트엔드를 자바스크립트, 백엔드를 자바로 구현한다고 가정하자.
데이터를 전송할 경우, 언어가 다르기 때문에 둘은 서로의 언어를 이해하지 못할 것이다.
따라서 서로 다른 언어로 데이터를 주고받으려고 할 때는 원시타입의 데이터를 주고받는 것이 기본이다.
직렬화란 객체의 내용을 바이트 단위로 변환하여 파일 또는 네트워크를 통해서 스트림(송수신)이 가능하도록 하는 것을 말한다.

 


◆ Protobuf 기능

프로토 버퍼는 OS가 뭐든, 플랫폼이 뭐든, 프로그래밍 언어가 뭐든 상관없이 한 곳에서 다른 곳으로 정보를 보내는 소프트웨어이다.

 

이는 컴퓨터 간 통신에 국한되어 있는 게 아니라, 파일을 디스크에 저장할 때 등 다양한 상황에서 사용 가능하다.

  • 만약 데이터를 저장할 때 프로토콜 버퍼에 맞춰서 파일에 썼다! 라고 한다면,
    이걸 끄집어내는 OS/언어/프로그램이 뭐든 간에 언제 어디서든지 동일하게 읽어낼 수 있는 것이다.

 

그래서 우리가 기본적으로 프로토 파일에 데이터를 표현을 하는 것이고, 데이터를 표현하기 위한 언어를 사용을 한 것이다.

  • 프로토 파일에 저렇게 규칙을 주고 그 규칙대로 해야 된다는 것 자체가 언어인 것.

 

 '프로토 버퍼 언어가 있고, 언어 버전도 있으며, 그 버전 안에는 문법이 있다' 

 


◆ Protobuf 동작 원리

어떻게 하면 플랫폼에 독립적이게 되는 걸까? 에 관한 이야기.

 

예제에서 프로토 파일에 정의한 메시지는 간단하게 멤버 엘리먼트를 하나만 뒀지만, 여러 개를 둘 수도 있음. 좀 더 구체적으로는 멤버 중에서도 필수적인 멤버(required), 선택적인 멤버(optional)을 정의해줄 수도 있음...

 

이처럼 프로토버퍼를 사용해 프로그래밍 언어들이 다루는 웬만한 자료구조의 형태는 표현 가능하다.

그러면 JSON을 이용한 직렬화와 프로토버퍼를 이용한 직렬화는 얼마나 차이가 날까?

위 그림은 각각 json과 Protocol buffer를 사용한 직렬화의 차이를 나타낸 것인데

  • json: 82byte
  • protocol buffer: 33 byte

와 같이 차이가 난다. 통신이 빨라짐.

 

 

 

그러면 프로토 버퍼의 동작 원리를 설명하기 위한 예시를 들어보자.

 

숫자를 비트로 표현한다고 하자. 

1바이트로 표현한다고 했을 때는, 오른쪽 끝을 2^0, 왼쪽 끝을 2^7로 표현하면 됨. 

(이때 오른쪽에 있는 것은 작은 값이니 Last-Significant하다고 하고, 왼쪽에 있는 것은 큰 값이니 Most-Significant하다고 한다.)

 

문제는 1바이트를 넘어서서, 2바이트가 필요할 때부터 발생한다.

사람의 경우, 2바이트를 표현하려고 하면 맨 오른쪽에서부터 2^0, 맨 왼쪽은 2^15 로 쓰면 된다.

 

그러나 컴퓨터 메모리는 바이트 단위로 쓴다.

1337을 통째로 16비트로 표현하지 않고, 13과 37로 쪼개서, 각각 1바이트 씩으로 표현한다는 뜻.

 

이때, 큰 쪽을 메모리에 먼저 저장할건지, 작은 쪽을 메모리에 먼저 저장할건지에 관해 의견이 갈렸다. 

'그냥 직관적으로 큰 값 먼저 넣고 그 뒤에 작은값 저장하자' vs '컴퓨터 메모리는 앞에 있는 주소가 더 작으니까, 작은 주소에 작은 값 넣고 큰 주소에 큰 값 넣자' 로 갈린 것.

 

이런 이유 때문에, CPU마다 메모리 저장 방식이 달라지게 되었음. 

 

프로그래밍 언어에서 쓸 때는 그냥 정수! 라고 쉽게 정의하면 되겠지만,

컴퓨터가 정보를 네트워크로 내보낼 때에는 어느 CPU, 어느 OS, 어느 컴퓨터가 받을지 모르는데, 못 읽으면 어떡하지?

→ 결국은 사람들은 이 모든 것을 정하기 시작함.

 

예를들어 1바이트 보낼 때는 Last Significant Bit부터, 2바이트 보낼 때는 Most Significant Bit부터, ...이런 식으로 약속을 한 것임. 

이게 조금만 틀려도 데이터가 깨지는 거임. 엄격하게 이루어져 있음.

 

 

따라서 결론!

프로토콜 버퍼는 쉽게 얘기하면 바이트 단위, 하드웨어 레벨의 원초적인 형태로 데이터를 표현한 것이다.

원초적인 형태로 바이트 레벨까지 내려가서 표현을 했으니 플랫폼에 따라 해석이 달라질 수가 없는 것.

뭘 먼저 보낼지, 어떤 형태로 보낼지 등 아주 디테일한 하드웨어적 정보까지 다 적어줌.

 


◆ gRPC의 장단점

 

장점

  • Performance
    • 프로토 버퍼 형태로 통신하기 때문에 빠르다.
    • HTTP/2 기반으로 동작한다. 
      • 비트 단위로 주고받고, 압축을 하므로 주고받는 데이터가 작다.
      • Multiplexing: head-of-line blocking을 개선.
  • Code generation
    • 코딩이 쉽고 편리하다. 
    • 프로토 파일만 공유하면, 프로토 컴파일러에 의해서 message나 function 등이 언어별로 자동으로 만들어짐.
  • Strict specification
    • 철학이 아닌, 명확하고 구체화된 명세가 있으므로 개발 시 소통이 편함
  • Streaming
  • Deadline/timeouts & cancellation
    • 제공해주긴 하는데, 아무래도 Best effort로 작동하긴 할 것.

 

단점

  • Limited browser support
    • HTTP/2만 지원하는데, 일반적으로 유저가 HTTP 어떤 버전을 쓸 지 결정할 수 있는 경우는 별로 없음.
  • Not human readable
    • 사람이 이해하기에는 어려운 형태.

 

주의할 점; Right tool for right problem

'CS > 풀스택서비스네트워킹' 카테고리의 다른 글

9. WebRTC  (0) 2023.12.08
8. HTTP/2  (0) 2023.12.08
6. HTTP/1.1  (0) 2023.10.16
5. ZeroMQ  (1) 2023.10.15
4. Socket  (0) 2023.09.30
Comments