gRPC Server 구현 과정 ( JAVA )

이준섭·2024년 7월 11일
0

gRPC

목록 보기
1/2
post-thumbnail

gRPC란?

gRPC는 Protocol Buffers(protobuf)를 사용하여 데이터를 직렬화하고, HTTP/2 프로토콜을 기반으로하여 양방향 스트리밍 및 다양한 기능을 제공합니다. 이를 통해 클라이언트와 서버 간의 효율적인 통신이 가능합니다.

먼저 gRPC 프로토콜을 정의하는 .proto 파일을 작성하고, 해당 파일을 사용하여 클라이언트 및 서버 코드를 자동으로 생성해야 합니다. 그런 다음, 생성된 코드를 사용하여 서비스의 비즈니스 로직을 구현하고 클라이언트와 서버 간의 통신을 설정할 수 있습니다.

1. Protocol Buffer을 정의하는 '.proto'파일 작성

위 예제에서는 첫 줄에 syntax = “proto3”을 지정해줌으로써 proto version 3의 규약을 따르겠다고 선언했습니다. 이를 명시하지 않으면 default로 version2 문법을 따르게 됩니다. 아래와 같이 지원 언어도 다르지만, message 작성 시 field rule 지정 등 문법에도 차이가 나타납니다.

  • Proto2 지원 언어 : C++, Java, Python, Go

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

  syntax = "proto3";

  package com.example.grpc;
  option java_outer_classname = "BiddingModel";

  // 통신 request/response 객체 정의
  message BidRequest {
    string userId = 1;
    int32 bidPrice = 2;
  }

  message BidResponse {
    int32 bidPrice = 1;
  }

  // 서비스 인터페이스를 정의
  service BidService {
    // 1:1
    rpc bidding(BidRequest) returns (BidResponse);
    // N:1
    rpc winBidding(BidRequest) returns (stream BidResponse);
    // N:N
    rpc streamBidding(stream BidRequest) returns (stream BidResponse);
  }

gRPC에서 제공하는 4가지 방식의 이해를 돕기 위한 그림입니다.

2. build.gradle 작성

  plugins {
      id 'java'
      id 'org.springframework.boot' version '3.2.5'
      id 'io.spring.dependency-management' version '1.1.4'
      // protobuf 플러그인
      id 'com.google.protobuf' version '0.8.15'
  }

  repositories {
      mavenCentral()
  }

  group = 'com.grpc'
  version = '0.0.1-SNAPSHOT'

  java {
      sourceCompatibility = '17'
  }

  configurations {
      compileOnly {
          extendsFrom annotationProcessor
      }
  }

  dependencies {
      implementation 'org.springframework.boot:spring-boot-starter'
      implementation 'org.springframework.boot:spring-boot-starter-web'
      implementation 'javax.annotation:javax.annotation-api:1.3.2'
      // grpc
      // grpc-java는 netty서버를 embedded하여 사용합니다
      implementation 'io.grpc:grpc-netty-shaded:1.36.0'
      implementation 'io.grpc:grpc-protobuf:1.36.0'
      implementation 'io.grpc:grpc-stub:1.36.0'

      // protobuf
      implementation "com.google.protobuf:protobuf-java-util:3.8.0"
      implementation "com.google.protobuf:protobuf-java:3.8.0"

      // grpc test
      testImplementation group: 'io.grpc', name: 'grpc-testing', version: '1.36.0'
      compileOnly 'org.projectlombok:lombok'
      annotationProcessor 'org.projectlombok:lombok'
      testImplementation 'org.springframework.boot:spring-boot-starter-test'
  }

  protobuf {
      protoc {
          artifact = 'com.google.protobuf:protoc:3.12.0'
      }
      plugins {
          grpc {
              artifact = 'io.grpc:protoc-gen-grpc-java:1.36.0'
          }
      }
      generateProtoTasks {
          all()*.plugins {
              grpc {}
          }
      }
  }

  sourceSets {
      main {
          // 빌드된 폴더를 인텔리제이가 인식할수 있도록 소스폴더로 포함
          java {
              srcDirs += [ './build/generated/source/proto/main/grpc', './build/generated/source/proto/main/java' ]
          }
          // 프로토콜버퍼 파일의 위치를 지정
          proto {
              srcDir 'src/main/resources/proto'
          }
      }
  }

  jar {
      enabled = false
  }

3. Protocol Buffers(.proto)에서 정의한 서비스 인터페이스 구현

  @Slf4j
  @Service
  public class BidService extends BidServiceGrpc.BidServiceImplBase {

      // Unary --> 1:1 방식
      @Override
      public void bidding(BiddingModel.BidRequest request, StreamObserver<BiddingModel.BidResponse> responseObserver) {
          try{
              log.info("bidding: request: {}", request);

              /**
               *  이 부분에 서비스 로직 구현
               */

              // gRPC가 구현해준 모델 클래스
              BiddingModel.BidResponse response = BiddingModel.BidResponse
                      .newBuilder()
                      .setBidPrice(request.getBidPrice())
                      .build();

              // 1개의 요청에 대한 1개의 응답을 처리한다.
              responseObserver.onNext(response);
              // 완료 이벤트
              responseObserver.onCompleted();
          } catch (Exception e) {
              responseObserver.onError(
                      Status.INTERNAL
                              .withDescription(e.getMessage())
                              .withCause(e)
                              .asRuntimeException()
              );
          }
      }
      // 서버 스트리밍 방식 --> 클라이언트의 요청이 시작되면 서버가 완료이벤트를 전송하기 전까지 클라이언트는 서버로부터 데이터를 받을수 있습니다. ( 1:N 방식 )
      @Override
      public void winBidding(BiddingModel.BidRequest request,StreamObserver<BiddingModel.BidResponse> responseObserver) {

          /**
           *  이 부분에 서비스 로직 구현
           */

          log.info("winBidding: request: {}", request);
          BiddingModel.BidResponse response = BiddingModel.BidResponse
                  .newBuilder()
                  .setBidPrice(request.getBidPrice())
                  .build();
          // 클라이언트에의 요청은 1번이지만 서버는 아래처럼 여러번의 데이터를 스트리밍 할 수 있습니다.
          responseObserver.onNext(response);
          responseObserver.onNext(response);
          responseObserver.onNext(response);
          responseObserver.onNext(response);
          responseObserver.onNext(response);
          responseObserver.onNext(response);

          responseObserver.onCompleted();
      }

      // 양방향 스트리밍 방식 --> 클라이언트에서 완료이벤트가 전송하기 전까지, 서로간의 스트리밍을 진행합니다. ( N:N 방식 )
      @Override
      public StreamObserver<BiddingModel.BidRequest> streamBidding(StreamObserver<BiddingModel.BidResponse> responseObserver) {
          return new StreamObserver<BiddingModel.BidRequest>() {
              int count = 0;
              @Override
              public void onNext(BiddingModel.BidRequest value) {
                  log.info("streamBidding request: {}", value);

                  /**
                   *  이 부분에 서비스 로직 구현
                   */

                  // 클라이언트로부터 데이터가 올 때마다 onNext가 호출된다
                  // 1개의 요청이 올 때마다 n번의 응답을 스트리밍 전송한다
                  BiddingModel.BidResponse response = BiddingModel.BidResponse
                          .newBuilder()
                          .setBidPrice(value.getBidPrice())
                          .build();
                  responseObserver.onNext(response);
                  responseObserver.onNext(response);
                  responseObserver.onNext(response);
              }

              @Override
              public void onError(Throwable t) {
                  responseObserver.onError(
                          Status.INTERNAL
                                  .withDescription(t.getMessage())
                                  .withCause(t)
                                  .asRuntimeException()
                  );
              }

              @Override
              public void onCompleted() {
                  log.info("streamBidding onCompleted");
                  // 클라이언트의 완료이벤트가 오면 서버도 완료이벤트를 진행한다
                  responseObserver.onCompleted();
              }
          };
      }
  }

4. Config Bean을 구성

  @Configuration
  @RequiredArgsConstructor
  public class ServerConfig {
      @Value("${grpc.port}")
      private Integer port;
      private final BidService bidService;

      @Bean
      public Server grpcServer() {
             return ServerBuilder
                     .forPort(port)
                     .addService(bidService)
                     .build();
      }
  }

5. 테스트 진행을 위해서 Application Runner 설정

  @Component
  @RequiredArgsConstructor
  public class ServerRunner implements ApplicationRunner, DisposableBean {

      private final Server grpcServer;

      @Override
      public void destroy() throws Exception {
          if(!ObjectUtils.isEmpty(grpcServer)) {
              grpcServer.shutdown();
          }
      }

      @Override
      public void run(ApplicationArguments args) throws Exception {
          grpcServer.start();
          grpcServer.awaitTermination();
      }
  }

실시간 입찰을 진행하는 환경에서 gRPC를 고려해보려고 예제를 만들어 보았습니다.
아래는 실시간 환경에서 gRPC가 갖고 있는 이점에 대한 설명 입니다.

1. 낮은 지연 시간과 높은 성능

- gRPC는 HTTP/2 기반으로 동작하여 다중화, 헤더 압축, 서버 푸시 등의 기능을 통해 
  요청과 응답의 지연 시간을 최소화합니다.
- Protocol Buffers를 사용하여 데이터를 직렬화하므로, 데이터 전송이 매우 효율적입니다. 
  이는 실시간 입찰 시스템에서 매우 중요한 빠른 응답을 가능하게 합니다.

2. 양방향 스트리밍 지원

gRPC는 단방향 및 양방향 스트리밍을 지원합니다. 이는 클라이언트와 서버 간의 지속적인 
데이터 흐름을 가능하게 하여, 실시간으로 입찰 상황을 업데이트하고, 
입찰자들이 실시간으로 상호작용할 수 있게 합니다.

3. 효율적인 데이터 전송

Protocol Buffers를 사용하여 데이터를 이진 형식으로 직렬화하기 때문에, 
네트워크 대역폭을 효율적으로 사용하면서도 데이터를 빠르게 전송할 수 있습니다.

4. 확장성과 분산 처리

gRPC는 분산 시스템과 마이크로서비스 아키텍처에 적합합니다. 여러 개의 서비스 간에 
효율적으로 통신할 수 있으며, 높은 확장성을 제공합니다.

5. 다양한 언어 지원

gRPC는 여러 프로그래밍 언어에서 클라이언트와 서버를 쉽게 구현할 수 있게 합니다. 
이는 다양한 플랫폼과 언어를 사용하는 환경에서 입찰 시스템을 구축할 때 유리합니다.

6. 보안

gRPC는 TLS를 기본적으로 지원하여, 데이터 전송의 보안성을 높입니다. 
이는 민감한 금융 정보를 다루는 실시간 입찰 시스템에서 중요한 요소입니다.

7. 서비스 정의와 코드 생성의 간편함

gRPC는 Protocol Buffers를 사용하여 서비스와 메시지를 정의하고, 
이를 통해 클라이언트와 서버 간의 통신 인터페이스를 자동으로 생성할 수 있습니다.
이는 개발자의 작업을 단순화하고, 일관된 인터페이스를 유지할 수 있게 합니다.

0개의 댓글