GRPC는 Groole Remote Procedure Call의 약자로 기존의 Client-Server의 HTTP 호출방식의 대안으로 떠오르는 IPC 기법입니다. 현대 시스템의 구조상 거대하고 복잡한 분산 환경 또는 MSA 환경이 주를 이루는데, 다양한 언어로 개발된 수많은 프로세스가 유기적으로 통신하면서 서비스를 제공합니다. 이런 복잡한 시스템 환경에서 일반적인 HTTP 통신보다 GRPC가 주는 이점이 많기 때문에 요즘 각광받고 있습니다.
RPC
우선 RPC에 대해 간단히 확인하고 본론으로 넘어가도록 하겠습니다.
RPC는 Remote Procedure Call 의 약자로 분산 네트워크 환경에서 조금 더 편하게 프로그래밍 하기 위한 등장하였습니다.
클라이언트 - 서버 간의 커뮤니케이션에 필요한 상세한 정보는 최대한 감추고 개발자는 각 로직에만 집중할 수 있도록 클라이언트/서버는 일반 메소드를 호출하는 것처럼 개발을 진행하면 됩니다.
- caller/callee
개발자가 필요한 비지니스 로직을 생성하고 정의된 IDL(interface definition language)로 작성하여 stub 을 호출합니다. - Stub
Stub compiler 가 IDL 파일을 읽어 원하는 language 로 생성하고 파라미터를 마샬링/언마샬링 처리하여 RPC 프로토콜로 전달합니다. - RPC runtime
통신하여 각 메세지를 전달하게 됩니다.
gRPC
그럼 이제 gRPC에 대해 확인을 해보도록 하겠습니다.
공식 페이지의 설명을 기반으로 간단하게 요약해 보았습니다. (https://grpc.io/)
gRPC는 google 에서 마이크로서비스에 사용하던 단일 범용 RPC 인프라 Stubby 에서 시작하였습니다. Stubby 다음 버전을 계확하면서 외부에 오픈하기로 결정하하였다고 하네요. 높은 생산성과 효율적인 유지보수, 다양한 언어와 플랫폼 지원, 높은 메세지 압축률과 성능, 이러한 특징으로 한마디로 높은 성능의 오픈소스 범용 RPC 프레임워크입니다.
각 특징에 대해 추가로 확인해보겠습니다.
높은 생산성과 효율적인 유지보수
IDL(Identity Definition Language)로 protocol buffers(protobuf) 사용합니다. IDL만 정의하면 높은 성능을 보장하는 서비스와 메세지에 대한 소스코드가 각 언어에 맞게 자동 생성됩니다.
따라서 개발자들은 생성된 코드를 클라이언트, 서버 간의 사용 언어에 구애받지 않고 사용하기만 하면 되며 정해진 규약을 공통으로 사용하기 때문에 의사소통 비용이 감소하게 됩니다.
다양한 언어와 플랫폼 지원
앞서 말씀드린 것과 같이 IDL 정의 한개로 다양한 언어와 플랫폼에서 동작하는 서버와 클라이언트 코드가 생성됩니다.
현재 공식 지원 언어들은 아래와 같습니다.
높은 메시지 압축률과 성능
gRPC 는 내부적으로 HTTP/2 사용하여 헤더 압축률이 높고 protobuf 에 의해 통신시점에는 바이너리 데이터로 통신하기 때문에 메시지 크기가 작습니다.
그리고 추가로 HTTP/2 이기 때문에 양방향 스트림 통신이 가능하죠.
단점
이런 장점들이 있지만 간단한 RESTful API 를 제공을 목적으로 한다면 부적합하죠. 그리고 protobuf 및 HTTP/2 에 대한 아주 약간의 러닝 커브가 존재합니다.
또한 일반 REST API 와 다르게 메세지가 바이너리로 전달하게 되어 개발자들의 테스트가 쉽지 않은데, 이건 조사하다보니 POSTMAN과 유사한 bloomRPC(https://github.com/uw-labs/bloomrpc) 툴을 통해 gRPC 테스트를 해볼 수 있더라고요.
Protocol Buffers
Protocol Buffers 에 대해서도 간단히 확인해 보겠습니다.
gRPC 에서 IDL 로 사용하고 있는 언어입니다. 구글에서 만들고 사용하는 데이터 직렬화 라이브러리로 아래와 같을 문법을 사용합니다.
이렇게 작성된 proto파일을 통해서 protoc 컴파일러를 통해 각 언어로 소스 코드가 생성됩니다.
아래와 같은 예시로 protocol buffer 가 선언되었고, 이는 통신하는 시점에 아래 이미지와 같이 인코딩되어 송수신됩니다. 따라서 본문의 사이즈가 훨씬 간결해지죠.
message Person {
required string user_name = 1;
optional int64 favourite_number = 2;
repeated string interests = 3;
}
Sample
이제 간단한 샘플 코드를 통해 gRPC 사용법에 대해 알아보고자 합니다.
JAVA로 서버로 구성하고 Python 으로 클라이언트를 구성하였고요. 단순하게 Member 등록하는 기능을 샘플로 만들어보았습니다.
1. proto생성
우선 proto 파일을 생성해야 합니다.
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.ks.grpc";
option java_outer_classname = "Member";
package grpc;
// enum 정의
enum Gender {
MALE = 0;
FEMALE = 1;
}
// message = object 정의
message Class {
// int32 -> int(java)
int32 classNo = 1;
// string -> String(java)
string className = 2;
}
message MemberRequest {
string name = 1;
// 위에 정의한 enum 값 활용
Gender gender = 2;
string phoneNo = 3;
repeated Class classList = 4;
}
message MemberResponse {
// int64 -> long(java)
int64 memberNo = 1;
}
// 자동 생성되는 클래스
service RegisterMember {
// 클래스 메소드 생성
//unary : 한 번 호출에 한 번 응답
rpc Register(MemberRequest) returns (MemberResponse);
//client stream : 클라이언트에서 스트림으로 전달, 서버에서는 한번 응답
rpc RegisterClientStream(stream MemberRequest) returns (MemberResponse);
//server stream : 클라이언트에서 한 번 전달, 서버에서 스트림으로 응답
rpc RegisterServerStream(MemberRequest) returns (stream MemberResponse);
//bi stream : 클라이언트/서버 모두 스트림으로 응답
rpc RegisterBiStream(stream MemberRequest) returns (stream MemberResponse);
}
syntax 로 사용할 문법을 선언합니다.
enum, message 로 Object를 정의하고 service 로 클래스가 생성됩니다.
service 안에는 메소드들을 선언해주면 되는데, 여기서 stream 문구를 통해 스트림 사용에 대해서 정의할 수 있습니다.
그에 따라 메소드 통신 방식이 일부 다르게 적용됩니다.
2. source generate
이제 위에 생성한 proto 파일을 통해 서버단인 자바와 파이썬에서 사용할 소스를 생성합니다.
각 언어별로 컴파일러가 존재하여 해당 컴파일러를 사용하여 소스를 생성하면 됩니다.
JAVA
자바에선 maven 기반으로 소스를 생성합니다.
proto 파일을 src/main/proto 폴더에 위치 해준 후에
grpc 용 dependency 를 추가하였고
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.18.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.18.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.18.0</version>
</dependency>
build 설정을 통해 compile 시점에 소스가 생성됩니다.
소스는 target 폴더 하위에 생성됩니다.
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.4.1.Final</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.5.0</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.4.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
python
클라이언트 샘플로 사용할 파이썬에서도 소스 생성을 하기 위한 준비가 필요합니다.
python 3.5 이상 버전이 필요하며 pip 버전 9.0.1 이상이 필요합니다.
라이브러리 설치가 우선 진행된 후
$ python -m pip install grpcio
$ python -m pip install grpcio-tools
라이브러리 명령어를 통해 소스를 생성합니다.
$ python -m grpc_tools.protoc -I./ --python_out=. --grpc_python_out=. ../sample.proto
3. 비지니스 로직 구현
이제 통신을 위한 준비는 모두 완료되었으며 각 어플리케이션에서 비지니스 로직을 구현하기만 하면 됩니다.
클라이언트에서는 메소드를 호출해주기만 하면 되며, 서버는 생성된 서비스를 상속받아 로직를 구현하기만 하면 완료됩니다.
JAVA Server
빌드되면서 생성된 서비스 클래스를 상속받아 각 메소드를 오버라이드하여 구현하면 서버 로직은 완료된다.
package com.ks.grpc.demo;
import com.ks.grpc.MemberRequest;
import com.ks.grpc.MemberResponse;
import com.ks.grpc.RegisterMemberGrpc;
import io.grpc.stub.StreamObserver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class MemberService extends RegisterMemberGrpc.RegisterMemberImplBase {
public static Logger logger = LoggerFactory.getLogger(MemberService.class);
//unary
@Override
public void register(MemberRequest request, StreamObserver<MemberResponse> responseObserver) {
logger.info("### unary Stream : {} ", request.toString());
MemberResponse response = MemberResponse.newBuilder()
.setMemberNo(1)
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
//client stream
@Override
public StreamObserver<MemberRequest> registerClientStream(StreamObserver<MemberResponse> responseObserver) {
StreamObserver<MemberRequest> stream = new StreamObserver<MemberRequest>() {
@Override
public void onNext(MemberRequest memberRequest) {
logger.info("### Client Stream : {} ", memberRequest.toString());
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onCompleted() {
MemberResponse response = MemberResponse.newBuilder()
.setMemberNo(1)
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
};
return stream;
}
//server stream
@Override
public void registerServerStream(MemberRequest request, StreamObserver<MemberResponse> responseObserver) {
logger.info("### Server Stream : {} ", request.toString());
MemberResponse response = MemberResponse.newBuilder()
.setMemberNo(1)
.build();
responseObserver.onNext(response);
responseObserver.onNext(response.toBuilder().setMemberNo(2).build());
responseObserver.onCompleted();
}
//bi stream
@Override
public StreamObserver<MemberRequest> registerBiStream(StreamObserver<MemberResponse> responseObserver) {
StreamObserver<MemberRequest> stream = new StreamObserver<MemberRequest>() {
@Override
public void onNext(MemberRequest memberRequest) {
logger.info("### Bi Stream : {} ", memberRequest.toString());
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onCompleted() {
MemberResponse response = MemberResponse.newBuilder()
.setMemberNo(1)
.build();
responseObserver.onNext(response);
responseObserver.onNext(response.toBuilder().setMemberNo(2).build());
responseObserver.onCompleted();
}
};
return stream;
}
}
그리고 gRPC에서 제공하는 서버 객체를 생성하여 port 와 service 를 등록하면 됩니다.
python client
python 에서는 channel을 생성 후 생성된 stub 소스를 통해 메소드를 호출해주기만 하면 요청이 완료됩니다.
import grpc
import sample_pb2
import sample_pb2_grpc
def run():
channel = grpc.insecure_channel('localhost:8877')
stub = sample_pb2_grpc.RegisterMemberStub(channel)
response = stub.Register(sample_pb2.MemberRequest(name="python",gender=0,phoneNo="01028406735"))
print("### Response : " + str(response.memberNo))
if __name__ == '__main__':
run()
- GRPC Web Project Sample
https://github.com/grpc/grpc-web
참고사이트
'IT 상식 > 개발 상식' 카테고리의 다른 글
[Network] HTTP(Hyper Text Transfer Protocol)의 기본 개념과 HTTPS와의 차이점 (0) | 2022.02.06 |
---|---|
[Network] HTTP/2 란? (0) | 2022.02.03 |
[Process] IPC(Inter-Process Communication)의 개념과 종류 (0) | 2022.01.23 |
[Process] 프로세스(Process)와 쓰레드(Thread)에 대한 이해 (0) | 2022.01.23 |
[REST API] REST API 설계 방법 (0) | 2022.01.23 |