Spring boot 에서 gRPC 간단히 사용해보기

By deuk9

최근 회사에서 gRPC 사용 가능성에 대한 이야기가 나왔다. 그래서 gRPC를 실제로 어떻게 사용할 수 있을지 간단하게 코드로 구성해보았다.

Java에서 gRPC를 사용할 때 보통은 net.devh:grpc-spring-boot-starter 기반으로 구성하는 샘플이 많지만, IntelliJ에서 Spring Boot 프로젝트를 새로 생성해보니 Spring에서 공식으로 제공하는 spring-grpc starter가 있어 이를 이용해보았다. 아직 버전은 1.0.0 미만이지만, 사용해보기에 충분했고 설정도 비교적 단순했다.

설정 관련 내용은 GitHub 샘플 코드에 정리해두었다.
이 글에서는 proto 작성부터 서비스 구현까지의 과정을 간단히 정리한다.

1. proto 파일 생성

syntax = "proto3";

package hello;
option java_multiple_files = true;
option java_package = "org.example.springgrpc.helloservice.proto";

service HelloService {
  rpc SayHello(HelloRequest) returns (HelloReply);
  rpc StreamHello(HelloRequest) returns (stream StreamHelloReply);
  rpc SayBye(ByeRequest) returns (ByeReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  repeated string message = 1;
  string byeMessage = 2;
}

message ByeRequest {
  string name = 1;
}

message ByeReply {
  string message = 1;
}

message StreamHelloReply {
  string message = 1;
}
  • QueryDSL과 마찬가지로 .proto 파일을 기반으로 Java 코드가 자동 생성된다.
  • package는 네임스페이스처럼 동작하며, 실제 호출 시 hello.HelloService.SayHello처럼 전체 경로가 붙는다.
  • service는 gRPC에서 제공하는 함수 목록이고, 각 함수의 입력과 반환 타입을 지정한다.
  • stream 키워드는 스트리밍 응답을 의미한다. 연결이 유지된 채로 여러 데이터를 연속으로 받을 수 있다.
  • message는 요청/응답 데이터 구조이며, repeated는 리스트 형태를 의미한다.

2. 코드 생성

  • .proto 파일을 작성한 후 generateProto task를 실행하면 Java 코드가 자동으로 생성된다.
  • 코드 생성 위치는 java_package에 지정한 패키지 기준이다.

3. 구현

3.1 gRPC Client 정의

@Configuration  
public class GrpcClientsConfig {  
  
    @Bean  
    HelloServiceGrpc.HelloServiceBlockingStub stub(GrpcChannelFactory channels) {  
        return HelloServiceGrpc.newBlockingStub(channels.createChannel("local"));  
    }  
  
    @Bean  
    HelloServiceGrpc.HelloServiceFutureStub asyncStub(GrpcChannelFactory channels) {  
        return HelloServiceGrpc.newFutureStub(channels.createChannel("local"));  
    }  
  
}
  • GrpcChannelFactory를 통해 설정한 채널 이름(local)을 기준으로 채널을 생성한다. (application.yml 참고)
  • 서비스마다 제공되는 BlockingStub, FutureStub 중 하나를 선택해 Bean으로 등록할 수 있다.

3.3 서비스 구현

@Slf4j  
@GrpcService  
@RequiredArgsConstructor  
public class HelloService extends HelloServiceGrpc.HelloServiceImplBase {  
  
    private final HelloServiceBlockingStub stub;  
    private final HelloServiceFutureStub asyncStub;  
  
    @Override  
    public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {  
        ListenableFuture<ByeReply> byeFuture = asyncStub.sayBye(  
            ByeRequest.newBuilder()  
                .setName(request.getName())  
                .build());  
  
        log.info("stream request");  
        Iterator<StreamHelloReply> streamHello = stub.streamHello(request);  
        log.info("stream response");  
        try {  
            ByeReply byeReply = byeFuture.get();  
            Builder builder = HelloReply.newBuilder()  
                .setByeMessage(byeReply.getMessage());  
  
            // streamHello Iterator의 각 StreamHelloReply에서 message를 추출하여 추가  
            while (streamHello.hasNext()) {  
                log.info("streamHello.hasNext()");  
                StreamHelloReply reply = streamHello.next();  
                builder.addMessage(reply.getMessage());  
            }  
            log.info("test");  
            HelloReply helloReply = builder.build();  
  
            log.info("HelloReply: {}", helloReply);  
            responseObserver.onNext(helloReply);  
            responseObserver.onCompleted();  
  
        } catch (InterruptedException | ExecutionException e) {  
            throw new RuntimeException(e);  
        }  
    }  
  
    @Override  
    public void streamHello(HelloRequest request,  
        StreamObserver<StreamHelloReply> responseObserver) {  
        for (int i = 0; i < 10; i++) {  
            try {  
                Thread.sleep(2000L);  
            } catch (InterruptedException e) {  
                throw new RuntimeException(e);  
            }  
            responseObserver.onNext(StreamHelloReply.newBuilder()  
                .setMessage("Hello " + request.getName() + " " + i)  
                .build());  
        }  
        try {  
            Thread.sleep(5000L);  
        } catch (InterruptedException e) {  
            throw new RuntimeException(e);  
        }  
        responseObserver.onCompleted();  
    }  
  
    @Override  
    public void sayBye(ByeRequest request, StreamObserver<ByeReply> responseObserver) {  
      responseObserver.onNext(ByeReply.newBuilder()  
           .setMessage("Bye " + request.getName())  
           .build());  
       responseObserver.onCompleted();  
       log.info("bye");  
    }  
}
  • @GrpcService는 gRPC 서버로 등록되기 위한 어노테이션이다. (@Service만 써도 되지만, 명시적으로 @GrpcService를 쓰면 가독성 측면에서 더 낫다.)
  • sayHello 메서드에서는 FutureStub으로 sayBye를 비동기 호출하고, BlockingStub으로 streamHello를 호출하여 응답을 조합한다.
  • 처음엔 stub.streamHello(request)에서 blocking이 걸릴 줄 알았는데, 실제로는 채널만 열리고, .next()로 값을 꺼낼 때 blocking이 발생한다. (다시 생각해보면 당연한 흐름이다.
  • streamHello는 2초 간격으로 메시지를 보내는 서버 스트리밍 처리 예제다.
  • sayBye는 단순한 unary 응답을 처리하며, 비동기로 구현했다.

마무리하며

Spring Boot에서 gRPC를 사용해보는 것은 꽤 간단했다. 공식 Spring starter인 spring-grpc 덕분에 설정도 복잡하지 않았고, proto 정의 → 코드 생성 → 서버/클라이언트 구성 → 실행까지 한 번에 정리할 수 있었다.