본문 바로가기

IT

gRPC란? RPC, gRPC, REST API

728x90

gRPC REST API 설계에 사용되는 2가지 방법이다. API 정의 프로토콜 세트를 사용하여 소프트웨어 구성 요소가 서로 통신할 있게 하는 메커니즘인데, gRPC에서는 구성 요소(해당 클라이언트) 다른 소프트웨어 구성 요소(해당 서버) 특정 함수를 직접 또는 간접적으로 호출하는 반면 REST에서는 함수를 직접적으로 호출하는 대신 클라이언트가 서버의 데이터를 요청하거나 업데이트를 요청한다.

 

해당 정의가 무슨 뜻인지 이해됐다면 해당 포스팅을 넘어가도 좋지만 개념이 조금이라도 흔들린다면 이 글을 읽고 도움이 되길 바란다. 

 

RPC(Google Remote Procedure Call)란 네트워크로 연결된 서버 상의 프로시저(함수, 메서드 ) 원격으로 호출할 있는 기능이다. 원격지의 자원을 내 것처럼 사용할 수 있다. IDL(Interface Definication Language) 기반으로 다양한 언어를 가진 환경에서도 쉽게 확장이 가능하며, 인터페이스 협업에도 용이하다는 장점이 있다.

※ IDL : 서비스 간의 통신을 위한 인터페이스를 정의한다는 의미로 해석하면 좋을 듯 하다. 
IDL은 서로 다른 시스템 간에 통신하는 서비스나 객체의 인터페이스를 명확하게 정의하기 위한 언어이다. 
여러 시스템에서 서로 다른 프로그래밍 언어로 개발된 애플리케이션은 통신할 때 데이터를 주고 받는데 있어서 
각 시스템에서 사용하는 언어의 데이터 타입, 구조, 메서드 호출 등이 서로 다르기 때문에 
이를 표준화하고 명시하기 위해 IDL을 사용한다.

 

 

그렇다면 gRPC(Google Remote Procedure Call)란? Google에서 만든 RPC(Remote Procedure Call) 프레임워크이다.

두가지 데이터 전송에 Protocol Buffer HTTP/2 사용하는 두가지 차이점을 알고 가면 좋을 듯 하다.

 

Protocol Buffer란 구글에서 개발한 직렬화 기법이다.(포맷과 그에 대한 라이브러리를 말한다) 프로토콜 버퍼는 경량이면서도 효율적이며, 이진 데이터의 직렬화와 역직렬화를 위한 표준화된 방법을 제공하며 주로 구조화된 데이터를 저장하고 통신하기 위해 사용된다.

 

Protocol Buffer의 다섯 가지 특징을  확인해보자.

 

1. 간결하고 효율적인 이진 포맷 

  • Protocol Buffer는 텍스트 기반 포맷(JSON, XML)보다 더 간결하며, 이진 데이터로 표현된다. 이는 크기가 작고 직렬화 및 역직렬화 과정에서 높은 효율을 제공한다. 
  • 직렬화란, 데이터 표현을 바이트 단위로 변환하는 작업을 의미하는데 아래 예제처럼 같은 정보를 저장해도 text 기반인 json인 경우 82 byte가 소요되는데 반해, 직렬화 된 protocol buffer는 필드 번호, 필드 유형 등을 1byte로 받아서 식별하고, 주어진 length 만큼만 읽도록 하여 단지 33 byte만 필요하게 된다고 한다. 자세한 설명은 해당 블로그를 참고하도록 하자.
  • https://martin.kleppmann.com/2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html 

Json vs Protocol Buffer

 

 

2. 스키마 정의

  • 데이터 구조를 정의하기 위해 Protocol Buffer 스키마(Proto 파일)를 사용한다. 이 스키마를 사용하면 데이터 모델을 명시적으로 정의할 수 있으며, 각 필드의 데이터 유형, 구조, 기본값 등을 설정할 수 있다.
syntax = "proto3";

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;
}

/**
Proto3 지원 언어 : C++, Java, Python, Go, Ruby, Objectice-C, C#, JavaScript, PHP, Dart

required : 필수로 가져야 할 필드 (only use proto2)

optional : 해당 필드를 가지지 않거나 하나만 가짐 (only use proto2)

repeated : 임의 반복 가능한 필드 (번호 및 값의 순서는 보존)
*/

 

3. 언어 중립적

  • Protocol Buffer는 다양한 프로그래밍 언어에서 사용할 수 있다. Protocol Buffer 스키마를 사용하여 각 언어에 맞는 코드를 자동으로 생성할 수 있다.

4. 확장성

  • 새로운 필드를 추가하거나 구조를 변경해도 기존의 코드에 영향을 주지 않고 역직렬화할 수 있다. 이는 버전 간의 호환성을 제공하며, 데이터 모델의 확장이 용이하다.

5. 빠른 직렬화 및 역직렬화 

  • 1번에서 확인한 바와 같이 Protocol Buffer는 이진 형식이므로 직렬화와 역직렬화가 빠르게 수행된다. 또한, 런타임 데이터의 크기가 작아서 효율적인 네트워크 통신이 가능하다. 

HTTP/2에 대한 장점을 간략하게 설명하자면 HTTP/1.1 기본적으로 클라이언트의 요청이 올때만 서버가 응답을 하는 구조로 요청마다 connection 생성해야만 한다. cookie 많은 메타 정보들을 저장하는 무거운 header 요청마다 중복 전달되어 비효율적이고 느린 속도를 보여준다. 이에 HTTP/2에서는 connection으로 동시에 여러 메시지를 주고 받으며, header 압축하여 중복 제거 전달하기에 version1 비해 훨씬 효율적이며 필요 클라이언트 요청 없이도 서버가 리소스를 전달할 수도 있기 때문에 클라이언트 요청을 최소화 있다.

HTTP/1.1과 HTTP/2

정리하자면 다음과 같은 특징을 가지고 있다.

1. 다중화 (Multiplexing):

  • 단일 TCP 연결을 통해 여러 개의 요청 및 응답을 동시에 처리할 수 있도록 다중화를 지원한다. 이는 여러 요청이 동시에 전송되어 네트워크의 대기 시간을 줄이고 성능을 향상시킬 수 있다

2. 헤더 압축 (Header Compression):

  • 이전 버전의 HTTP에서는 헤더 정보가 중복되어 전송되어 대역폭을 낭비하는 경우가 많았지만 HTTP/2는 헤더를 압축하여 전송하므로, 네트워크 사용량을 줄이고 로드 타임을 감소시킨다.

3. 우선순위 지원 (Priority and Dependency):

  • HTTP/2에서는 각각의 요청에 대해 우선순위를 지정하고 의존성을 설정할 수 있다. 이를 통해 중요한 리소스에 대한 로딩을 최우선으로 처리할 수 있다.

4. 서버 푸시 (Server Push):

  • HTTP/2는 서버 푸시를 지원하여 클라이언트 요청에 대한 리소스를 서버가 미리 예측하여 전송할 수 있습니다. 이로써 클라이언트는 추가 요청을 보내지 않아도 되어 페이지 로딩 속도가 향상된다.

gRPC(Google Remote Procedure Call)의 Protocol Buffer HTTP/2에 대해 알아보았으니 gRPC를 Java/Spring을 활용하여 간단한 예제코드를 작성해보고 REST 통신과의 차이에 대해 감을 잡아보자.

 

Protocol Buffer 정의

example.proto

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.example.grpc";
option java_outer_classname = "ExampleProto";

package example;

message HelloRequest {
  string name = 1;
}

message HelloResponse {
  string greeting = 1;
}

service ExampleService {
  rpc sayHello (HelloRequest) returns (HelloResponse);
}

 

Protocol Buffer를 컴파일하여 자바코드 생성

pom.xml 

<build>
    <plugins>
        <plugin>
            <groupId>org.xolstice.maven.plugins</groupId>
            <artifactId>protobuf-maven-plugin</artifactId>
            <version>0.6.1</version>
            <configuration>
                <protocArtifact>com.google.protobuf:protoc:3.11.0</protocArtifact>
                <pluginId>grpc-java</pluginId>
                <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.31.0</pluginArtifact>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <goal>compile-custom</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

 

gRPC 서비스 구현 (9090 port listening)

ExampleServiceImpl.java

import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;

@GrpcService
public class ExampleServiceImpl extends ExampleServiceGrpc.ExampleServiceImplBase {

    @Override
    public void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
        String greeting = "Hello, " + request.getName() + "!";
        HelloResponse response = HelloResponse.newBuilder().setGreeting(greeting).build();
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

 

gRPC 클라이언트 구현

//gRPC의 ManagedChannel을 생성
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 9090)
                .usePlaintext() 
                .build();

//gRPC 스텁(Stub)을 생성, 동기 호출을 위한 blockingStub 생성
ExampleServiceGrpc.ExampleServiceBlockingStub blockingStub = ExampleServiceGrpc.newBlockingStub(channel);

//protocol buffer에서 정의한 HelloRequest의 빌더 생성
HelloRequest request = HelloRequest.newBuilder().setName("John").build();

//gRPC 동기 호출 -> 요청에 대한 응답을 기다림 
HelloResponse response = blockingStub.sayHello(request);

System.out.println("Response: " + response.getGreeting());

//gRPC 채널을 닫아 자원 해제
channel.shutdown();

 

설명은 주석을 참고하면 될 듯하고 스텁(Stub)에 대해서는 알 필요가 있다.

gRPC에서 스텁(Stub)은 클라이언트 측에서 서버와 통신할 수 있도록 도와주는 객체라고 생각하면 될 듯 하다. gRPC 스텁은 서버의 메서드를 호출하고, 요청 및 응답을 처리하는 역할을 수행한다. gRPC의 프로토콜 버퍼에서 정의한 서비스 메서드를 호출하는 데 사용하며 프로토콜 버퍼 파일을 기반으로 서비스와 메서드를 정의하고, 스텁을 생성하여 클라이언트 코드에서 해당 서비스를 사용할 수 있도록 돕는다.

스텁(Stub)은 서버와의 통신을 추상화하고 클라이언트 코드에서 더 쉽게 gRPC 서비스를 사용할 수 있도록 한다. 이를 통해 클라이언트 개발자는 서비스의 메서드를 호출하고, 데이터를 직렬화  역직렬화하는 복잡한 작업을 몰라도 되며, 편리하게 gRPC 서비스를 사용할  

다.

 

gRPC에서는 두 가지 주요 종류의 스텁(Stub)이 있다.

Blocking Stub (블로킹 스텁): 동기 방식으로 RPC 호출을 수행한다. 클라이언트는 호출이 완료될 때까지 대기하며, 서버의 응답을 받은 후에 다음 동작을 수행한다. 

Async Stub (비동기 스텁): 비동기 방식으로 RPC 호출을 수행한다. 클라이언트는 호출이 완료될 때까지 대기하지 않고, 콜백을 통해 비동기적으로 서버의 응답을 처리할 수 있다. 

 

 

AWS document에도 gRPC API와 REST API의 차이에 대해 간략하게 정리가 잘 되어있어서 참고하면 좋을 듯 하다.

https://aws.amazon.com/ko/compare/the-difference-between-grpc-and-rest/

   REST API와의

gRPC API vs REST API

 

 실시간 스트리밍과 대규모 데이터 로드가 필요한 내부 시스템에 더 적합한 gRPC는 시간이 지나도 API가 변경될 가능성이 낮은, 여러 프로그래밍 언어로 구성된 마이크로서비스 아키텍처에도 적합하다고 한다. 고성능 시스템, 많은 양의 데이터 로드, 실시간 또는 스트리밍 애플리케이션 환경에서 유용하게 사용된다고 하는데 서비스 도메인과 비즈니스에 적절한 통신 방법을 찾아 채택하는 과정을 겪어보고 싶다.