Spring gRPC

업데이트:

Spring gRPC1

  • Spring gRPC는 gRPC2 프로젝트를 간소화하여 구성하기위한 프로젝트이다.
  • gRPC란 고성능 오픈 소스 Remote Procedure Call(이하 RPC) Framework로, Client를 Server와 효율적으로 연결할 수 있다.
  • Protocol buffer3는 언어와 플랫폼에 중립적인 구조화된 데이터를 직렬화하기 위한 데이터 형식으로, JSON과 비슷하지만 더 빠르고 다양한 언어에서 쉽게 읽고 쓸 수 있다. 현재는 Protocol buffer v3인 proto3로 더 다양한 확장성을 제공한다.

Spring gRPC Sample

  • 간단한 Server-Client로 구성된 프로젝트를 기반으로 설명을 진행한다.

hello.proto

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.gracefulsoul.grpc.lib.proto";
option java_outer_classname = "HelloProto";

service Hello {
  rpc SayHello(Request) returns (Reply) {}
  rpc StreamHello(Request) returns (stream Reply) {}
}

message Request {
  string name = 1;
}

message Reply {
  string message = 1;
}
  • syntax는 Protocol buffer의 사용 버전을 명시하며, proto2 혹은 proto3를 반드시 명시한다.
  • option은 세부 설정을 위한 기능으로, 아래 세 가지를 정의하였다.
    • ‘java_package는’ Java 혹은 Kotlin 코드를 생성할 때, 패키지 이름을 정의한다.
    • ‘java_multiple_files’은 Java 코드를 생성할 때, .proto 파일을 단일 .java 파일로 생성할지 각 Java Class, Enum 등에 대해서 각각 생성할지 결정하는 설정으로 기본값은 false인 단일 .java 파일로 생성한다.
    • ‘java_outer_classname’는 Java 코드를 생성할 때, .proto 파일을 .java 파일로 생성할 때 이름을 결정하기 위한 설정으로 기본값은 .proto 파일의 이름을 카멜 표현식으로 변환한 값이다.
  • service는 message 타입을 RPC과 함께 사용하기 위해 서비스 인터페이스를 정의하면 Protocol buffer 컴파일러가 선택한 언어로 서비스 인터페이스 코드와 Client와 Server 간의 통신을 추상화하여 복잡한 부분을 숨기고, 사용자가 편리하게 서비스를 이용할 수 있도록 돕기위한 코드인 stubs을 생성한다.
    • rpc 키워드를 통해서 각 RPC를 정의하고, 반환되는 값에 stream 유무의 차이는 단일 응답인지 Streaming 응답인지를 구분하기 위한 키워드이다.
  • message는 Client와 Server 간의 주고 받을 내용을 정의하는 데이터 구조로, 필드(field)와 값(value)으로 구성된 구조화된 데이터 형식을 사용한다.
    • 위의 Request message를 예로 들면, string 타입의 name 필드 1개의 키-값 쌍을 가진다.

Request.java

// Generated by the protocol buffer compiler.  DO NOT EDIT!
// NO CHECKED-IN PROTOBUF GENCODE
// source: hello.proto
// Protobuf Java Version: 4.30.2

package com.gracefulsoul.grpc.lib.proto;

/**
 * Protobuf type {@code Request}
 */
public final class Request extends
    com.google.protobuf.GeneratedMessage implements
    // @@protoc_insertion_point(message_implements:Request)
    RequestOrBuilder {
private static final long serialVersionUID = 0L;
  static {
    com.google.protobuf.RuntimeVersion.validateProtobufGencodeVersion(
      com.google.protobuf.RuntimeVersion.RuntimeDomain.PUBLIC,
      /* major= */ 4,
      /* minor= */ 30,
      /* patch= */ 2,
      /* suffix= */ "",
      Request.class.getName());
  }
  // Use Request.newBuilder() to construct.
  private Request(com.google.protobuf.GeneratedMessage.Builder<?> builder) {
    super(builder);
  }
  private Request() {
    name_ = "";
  }
  // 이하 생략
}
  • 위의 ‘hello.proto’를 빌드하여 생성된 파일 중 Request message에 대한 파일로, 위에서 간단히 세 줄로 설정한 데이터 구조를 Java에 맞추어 객체 생성에 도움이 될 Builder를 포함하여 기본 객체 비교에 대한 Object의 메서드들과 데이터 송수신에 효율적인 구조를 정의한 GeneratedMessage의 각 메서드들을 상속받아 자동으로 구현한 객체이다.

HelloGrpc.java

package com.gracefulsoul.grpc.lib.proto;

import static io.grpc.MethodDescriptor.generateFullMethodName;

/**
 */
@io.grpc.stub.annotations.GrpcGenerated
public final class HelloGrpc {

  // 중략

  /**
   * A stub to allow clients to do asynchronous rpc calls to service Hello.
   */
  public static final class HelloStub
      extends io.grpc.stub.AbstractAsyncStub<HelloStub> {
    private HelloStub(
        io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
      super(channel, callOptions);
    }

    @java.lang.Override
    protected HelloStub build(
        io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
      return new HelloStub(channel, callOptions);
    }

    /**
     */
    public void sayHello(com.gracefulsoul.grpc.lib.proto.Request request,
        io.grpc.stub.StreamObserver<com.gracefulsoul.grpc.lib.proto.Reply> responseObserver) {
      io.grpc.stub.ClientCalls.asyncUnaryCall(
          getChannel().newCall(getSayHelloMethod(), getCallOptions()), request, responseObserver);
    }

    /**
     */
    public void streamHello(com.gracefulsoul.grpc.lib.proto.Request request,
        io.grpc.stub.StreamObserver<com.gracefulsoul.grpc.lib.proto.Reply> responseObserver) {
      io.grpc.stub.ClientCalls.asyncServerStreamingCall(
          getChannel().newCall(getStreamHelloMethod(), getCallOptions()), request, responseObserver);
    }
  }

  /**
   * A stub to allow clients to do limited synchronous rpc calls to service Hello.
   */
  public static final class HelloBlockingStub
      extends io.grpc.stub.AbstractBlockingStub<HelloBlockingStub> {
    private HelloBlockingStub(
        io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
      super(channel, callOptions);
    }

    @java.lang.Override
    protected HelloBlockingStub build(
        io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
      return new HelloBlockingStub(channel, callOptions);
    }

    /**
     */
    public com.gracefulsoul.grpc.lib.proto.Reply sayHello(com.gracefulsoul.grpc.lib.proto.Request request) {
      return io.grpc.stub.ClientCalls.blockingUnaryCall(
          getChannel(), getSayHelloMethod(), getCallOptions(), request);
    }

    /**
     */
    public java.util.Iterator<com.gracefulsoul.grpc.lib.proto.Reply> streamHello(
        com.gracefulsoul.grpc.lib.proto.Request request) {
      return io.grpc.stub.ClientCalls.blockingServerStreamingCall(
          getChannel(), getStreamHelloMethod(), getCallOptions(), request);
    }
  }

  // 이하 생략

}
  • ‘HelloGrpc.java’는 ‘hello.proto’에서 정의한 Hello service의 각 rpc를 수행하기 위한 객체로, Asynchronous 방식의 연동을 제공하는 HelloStub과 Synchronous 방식의 연동을 제공하는 HelloBlockingStub 등을 포함하여 다양한 기능 수행에 필요한 Stub 객체들을 컴파일을 통해 자동으로 만들어준다.

GrpcServerService.java (Server)

package com.gracefulsoul.grpc.server.service;

import org.springframework.stereotype.Service;
import com.gracefulsoul.grpc.lib.proto.HelloGrpc;
import com.gracefulsoul.grpc.lib.proto.Reply;
import com.gracefulsoul.grpc.lib.proto.Request;

import io.grpc.stub.StreamObserver;

@Service
public class GrpcServerService extends HelloGrpc.HelloImplBase {

	@Override
	public void sayHello(Request request, StreamObserver<Reply> responseObserver) {
		if (request.getName().startsWith("error")) {
			throw new IllegalArgumentException("Bad name: " + request.getName());
		}
		if (request.getName().startsWith("internal")) {
			throw new RuntimeException();
		}
		Reply reply = Reply.newBuilder().setMessage("Hello " + request.getName()).build();
		responseObserver.onNext(reply);
		responseObserver.onCompleted();
	}

	@Override
	public void streamHello(Request request, StreamObserver<Reply> responseObserver) {
		int count = 0;
		while (count < 10) {
			Reply reply = Reply.newBuilder().setMessage("Hello " + request.getName()).build();
			responseObserver.onNext(reply);
			count++;
			try {
				Thread.sleep(1000L);
			} catch (InterruptedException e) {
				Thread.currentThread().interrupt();
				responseObserver.onError(e);
				return;
			}
		}
		responseObserver.onCompleted();
	}

}
  • 서버에서 각 호출에 대해서 응답을 제공하기 위한 RPC를 Hello service를 구현한 가장 기본 객체인 HelloImplBase를 상속받아 구현하였다.
  • 주요 차이점은 아래와 같다.
    • sayHello(Request request, StreamObserver<Reply> responseObserver) 메서드는 request로 전달받은 요청을 responseObserver를 이용하여 reply를 한 번에 전달하고 전송을 종료한다.
    • streamHello(Request request, StreamObserver<Reply> responseObserver) 메서드는 request로 전달받은 요청을 responseObserver를 이용하여 원하는만큼 분할하여 여러 번 전송하고 전송을 종료한다.

GrpcClientApplication.java (Client)

package com.gracefulsoul.grpc.client;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.grpc.client.GrpcChannelFactory;

import com.gracefulsoul.grpc.lib.proto.HelloGrpc;
import com.gracefulsoul.grpc.lib.proto.Request;

@SpringBootApplication
public class GrpcClientApplication {

	private static final Log LOG = LogFactory.getLog(GrpcClientApplication.class);

	public static void main(String[] args) {
		SpringApplication.run(GrpcClientApplication.class, args);
	}

	@Bean
	HelloGrpc.HelloBlockingStub stub(GrpcChannelFactory channels) {
		return HelloGrpc.newBlockingStub(channels.createChannel("local"));
	}

	@Bean
	CommandLineRunner runner(HelloGrpc.HelloBlockingStub stub) {
		return args -> LOG.info(stub.sayHello(Request.newBuilder().setName("GracefulSoul").build()));
	}

}
  • stub(GrpcChannelFactory channels) 메서드를 통해서 Synchronous 방식의 HelloBlockingStub을 테스트를 위해 ‘local’로 채널을 생성한 객체를 만들어준다.
  • runner(HelloGrpc.HelloBlockingStub stub) 메서드로 서버측의 sayHello RPC에 ‘GracefulSoul’을 전달해준다.
  • 결과는 아래와 같다.
    message: "Hello GracefulSoul"
    

Conclusion

  • 현재 서비스의 아키텍쳐가 다양해짐에 따라 구성되는 서비스의 언어 또한 필요에 따라 다양한 언어들로 구성을 하여 서로의 장점을 이용한 최적의 서비스를 제공하고있다.
  • 기존엔 REST API를 설계 후 Request와 Response에 대한 상세 문서로 연관 서비스 개발자들과 공유하고 관리하는 시간 또한 생산성의 저하의 주 요인이었다면, gRPC를 통해 자동화하고 정형화된 코드 기반으로 개발자들은 비즈니스 로직에 집중할 수 있다.
  • 실 운영에서 사용해보면 단순한 REST API보다는 러닝 커브와 디버깅에 대한 다양한 이슈를 접하겠지만, 익숙해지면 더 나은 개발 환경을 여러분에게 제공하게 될 것이다.

Reference

※ Sample Code는 여기에서 확인 가능합니다.

댓글남기기