Spring Boot 4.1でgRPC入門:サーバー・クライアント実装をやさしく解説

Spring Boot 4.1では、gRPCサーバー・クライアントを作りやすくするための公式サポートが追加されました

これまでSpring BootでgRPCを使う場合、外部のgRPC用Starterを追加したり、NettyサーバーやStub生成の設定を自分で組み立てたりする必要がありました。
Spring Boot 4.1では、spring-boot-starter-grpc-serverspring-boot-starter-grpc-client が用意され、Spring Bootらしい自動構成でgRPCを扱いやすくなっています。

この記事では、Spring Boot 4.1を使って、シンプルなgRPCサーバーとクライアントを作る流れを紹介します。

対象読者は、次のような人です。

・Spring BootでgRPCを試してみたい人
・REST API以外の通信方式を学びたい人
・Spring Boot 4.1の新機能を追いたい人
・gRPCサーバーとクライアントの最小構成を知りたい人
・マイクロサービス間通信にgRPCを使うか検討している人

gRPCとは?REST APIと何が違うのか

gRPCは、Googleが中心となって開発したRPCフレームワークです。

REST APIでは、URLとHTTPメソッドを使い、JSONでデータをやり取りすることが多いです。

一方、gRPCでは .proto ファイルにサービス定義とメッセージ定義を書き、その定義からJavaコードを自動生成します。通信にはProtocol Buffersが使われます。

イメージとしては、次のような違いです。

REST API:
  GET /users/1
  JSONでレスポンスを受け取る

gRPC:
  UserService.GetUser(UserRequest)
  Protocol Buffersでレスポンスを受け取る

REST APIはブラウザやcurlで確認しやすく、外部公開APIに向いています。

一方でgRPCは、型安全で通信効率がよく、サービス間通信に向いています。特にマイクロサービス構成では、内部APIとしてgRPCを使う選択肢があります。

ざっくり使い分けるなら、以下のようなイメージです。

外部公開API:
  REST API

社内・サービス間通信:
  gRPC

大量データやストリーミング:
  gRPC

ブラウザやフロントエンドから扱いやすいAPI:
  REST API

この記事では、まず一番シンプルな「名前を送ると挨拶を返す」gRPCアプリを作ります。

gRPC Client
  ↓
HelloService.SayHello
  ↓
gRPC Server

リクエストで "Spring" を送ると、レスポンスとして "Hello, Spring!" を返す構成です。

プロジェクト構成とGradle設定

まず、プロジェクト構成は以下のようなイメージです。

src
 ├─ main
 │   ├─ java
 │   │   └─ com.example.demo
 │   │       ├─ GrpcSampleApplication.java
 │   │       ├─ HelloGrpcService.java
 │   │       └─ HelloClientRunner.java
 │   ├─ proto
 │   │   └─ hello.proto
 │   └─ resources
 │       └─ application.yml
 └─ test
     └─ java
         └─ com.example.demo
             └─ HelloGrpcServiceTest.java

ポイントは、src/main/proto.proto ファイルを置くことです。

.proto ファイルから HelloServiceGrpcHelloRequestHelloReply などのJavaクラスが生成されます。サーバー側もクライアント側も、この生成コードを使って実装します。

Gradleでは、Spring BootのgRPCサーバー・クライアント用Starterと、Protobufコード生成用のプラグインを追加します。

plugins {
    id 'java'
    id 'org.springframework.boot' version '4.1.0'
    id 'io.spring.dependency-management' version '1.1.7'
    id 'com.google.protobuf' version '0.10.0'
}

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

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-grpc-server'
    implementation 'org.springframework.boot:spring-boot-starter-grpc-client'

    implementation 'io.grpc:grpc-stub'
    implementation 'io.grpc:grpc-protobuf'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.boot:spring-boot-starter-grpc-server-test'
    testImplementation 'org.springframework.boot:spring-boot-starter-grpc-client-test'
}

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

ここで重要なのは、次の3つです。

・spring-boot-starter-grpc-server を追加する
・spring-boot-starter-grpc-client を追加する
・.proto からJavaコードを生成するために protobuf プラグインを使う

REST APIだけのSpring Bootアプリと比べると、.proto からコード生成する分、少し設定が増えます。

ただし、一度仕組みを作ってしまえば、API定義を .proto に集約できるので、サーバーとクライアントの型ずれを防ぎやすくなります。

proto定義とgRPCサーバー実装

次に、src/main/proto/hello.proto を作成します。

syntax = "proto3";

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

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

この .proto では、HelloService というサービスを定義しています。

SayHello は、HelloRequest を受け取り、HelloReply を返すRPCメソッドです。

Javaっぽく見ると、次のようなイメージです。

HelloReply sayHello(HelloRequest request);

この .proto を元に、Gradleのビルド時にJavaコードが生成されます。

次に、サーバー側の処理を実装します。

package com.example.demo;

import com.example.demo.grpc.HelloReply;
import com.example.demo.grpc.HelloRequest;
import com.example.demo.grpc.HelloServiceGrpc;
import io.grpc.stub.StreamObserver;
import org.springframework.grpc.server.service.GrpcService;

@GrpcService
public class HelloGrpcService extends HelloServiceGrpc.HelloServiceImplBase {

    @Override
    public void sayHello(
            HelloRequest request,
            StreamObserver<HelloReply> responseObserver
    ) {
        String name = request.getName();

        HelloReply reply = HelloReply.newBuilder()
                .setMessage("Hello, " + name + "!")
                .build();

        responseObserver.onNext(reply);
        responseObserver.onCompleted();
    }
}

@GrpcService を付けることで、Spring BootがこのクラスをgRPCサービスとして検出します。

REST APIでいう @RestController に近い役割だと考えるとわかりやすいです。

REST API:
  @RestController

gRPC:
  @GrpcService

ただし、REST APIのようにURLを直接定義するのではなく、.proto に定義したサービス名とメソッド名で呼び出されます。

レスポンスは StreamObserver に渡します。

responseObserver.onNext(reply);
responseObserver.onCompleted();

最初は少し見慣れないですが、「レスポンスを1件返して完了する」と考えれば大丈夫です。

application.ymlとクライアント実装

次に、gRPCサーバーとクライアントの設定を application.yml に書きます。

spring:
  application:
    name: spring-boot-grpc-sample

  grpc:
    server:
      port: 9090
    client:
      channel:
        hello:
          target: static://localhost:9090
          negotiation-type: plaintext

NettyベースのgRPCサーバーでは、通常のWeb APIで使う server.port とは別に、spring.grpc.server.port を使います。

spring:
  grpc:
    server:
      port: 9090

つまり、REST APIが 8080、gRPCが 9090 というように、別ポートで待ち受ける構成にできます。

クライアント側では、hello というチャネル名に対して接続先を指定しています。

spring:
  grpc:
    client:
      channel:
        hello:
          target: static://localhost:9090
          negotiation-type: plaintext

ローカル開発では plaintext で問題ありません。
本番環境ではTLSや認証を検討する必要があります。

次に、アプリケーションクラスです。

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.grpc.client.ImportGrpcClients;

@SpringBootApplication
@ImportGrpcClients(
        target = "hello",
        types = com.example.demo.grpc.HelloServiceGrpc.HelloServiceBlockingStub.class
)
public class GrpcSampleApplication {

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

@ImportGrpcClients を使うことで、生成されたgRPCクライアントStubをSpring Beanとして扱えるようになります。

今回は target = "hello" を指定しています。
この hello は、先ほど application.yml に書いたチャネル名と対応します。

spring:
  grpc:
    client:
      channel:
        hello:
          target: static://localhost:9090

次に、起動時にgRPCサーバーを呼び出すクライアント処理を作ります。

package com.example.demo;

import com.example.demo.grpc.HelloReply;
import com.example.demo.grpc.HelloRequest;
import com.example.demo.grpc.HelloServiceGrpc;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class HelloClientRunner implements ApplicationRunner {

    private final HelloServiceGrpc.HelloServiceBlockingStub helloStub;

    public HelloClientRunner(HelloServiceGrpc.HelloServiceBlockingStub helloStub) {
        this.helloStub = helloStub;
    }

    @Override
    public void run(ApplicationArguments args) {
        HelloRequest request = HelloRequest.newBuilder()
                .setName("Spring Boot 4.1")
                .build();

        HelloReply reply = helloStub.sayHello(request);

        System.out.println("gRPC response: " + reply.getMessage());
    }
}

HelloServiceBlockingStub をコンストラクタで受け取っています。

public HelloClientRunner(HelloServiceGrpc.HelloServiceBlockingStub helloStub) {
    this.helloStub = helloStub;
}

Spring BootがStubをBeanとして登録してくれるため、通常のServiceやRepositoryと同じようにDIできます。

アプリケーションを起動します。

./gradlew bootRun

Windowsの場合は以下です。

gradlew.bat bootRun

起動に成功すると、コンソールに次のようなログが出ます。

gRPC response: Hello, Spring Boot 4.1!

これで、Spring Bootアプリの中でgRPCサーバーとクライアントが動作しました。

grpcurlで外部から呼び出す

gRPCの動作確認には grpcurl が便利です。

REST APIでいうcurlのgRPC版のようなものです。

grpcurl -plaintext \
  -d '{"name":"Spring"}' \
  localhost:9090 \
  HelloService/SayHello

レスポンス例です。

{
  "message": "Hello, Spring!"
}

もしサービス名が見つからない場合は、まずサービス一覧を確認します。

grpcurl -plaintext localhost:9090 list

.protopackage を指定している場合、呼び出し名が変わることがあります。
その場合は、grpcurl list で表示された名前を使って呼び出しましょう。

テストを書く

Spring Boot 4.1では、gRPCアプリケーションのテストもしやすくなっています。

@AutoConfigureTestGrpcTransport を使うと、通常のネットワークポートを使わず、テスト専用のin-processチャネルでgRPC通信をテストできます。

package com.example.demo;

import com.example.demo.grpc.HelloReply;
import com.example.demo.grpc.HelloRequest;
import com.example.demo.grpc.HelloServiceGrpc;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.grpc.client.ImportGrpcClients;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@AutoConfigureTestGrpcTransport
@ImportGrpcClients(types = HelloServiceGrpc.HelloServiceBlockingStub.class)
class HelloGrpcServiceTest {

    @Autowired
    private HelloServiceGrpc.HelloServiceBlockingStub helloStub;

    @Test
    void sayHelloで挨拶メッセージを返す() {
        HelloRequest request = HelloRequest.newBuilder()
                .setName("Test")
                .build();

        HelloReply reply = helloStub.sayHello(request);

        assertThat(reply.getMessage()).isEqualTo("Hello, Test!");
    }
}

このテストでは、実際に 9090 ポートを開かずにgRPCサービスを検証できます。

REST ControllerのテストでMockMvcを使うように、gRPCでもSpring Bootのテストサポートに乗せて確認できるのが便利です。

よくあるエラーと確認ポイント

生成コードが見つからない

以下のようなエラーが出ることがあります。

cannot find symbol
HelloServiceGrpc
HelloRequest
HelloReply

これは、.proto からJavaコードが生成されていないときによく起きます。

確認ポイントは以下です。

・src/main/proto 配下に .proto を置いているか
・protobuf Gradleプラグインを設定しているか
・./gradlew generateProto を実行したか
・IDEが generated source を認識しているか

IntelliJ IDEAを使っている場合、生成されたソースディレクトリが自動で認識されないことがあります。
その場合は、Generated Sources Rootとして認識されているか確認してください。

9090ポートが使われている

以下のようなエラーが出た場合です。

Address already in use

gRPCサーバーのポートが他のプロセスと競合しています。

application.yml でポートを変更します。

spring:
  grpc:
    server:
      port: 19090
    client:
      channel:
        hello:
          target: static://localhost:19090
          negotiation-type: plaintext

サーバー側だけでなく、クライアント側の接続先も合わせて変更してください。

クライアントが接続できない

以下のようなエラーが出ることがあります。

UNAVAILABLE: io exception

この場合は、次を確認します。

・gRPCサーバーが起動しているか
・spring.grpc.server.port が正しいか
・client.channel の target が正しいか
・plaintext / TLS の設定が一致しているか
・localhost と 0.0.0.0 の使い分けが正しいか

ローカル検証では、まず以下のようにシンプルにして確認するとよいです。

spring:
  grpc:
    client:
      channel:
        hello:
          target: static://localhost:9090
          negotiation-type: plaintext

REST APIのポートとgRPCのポートを混同する

Spring BootのWeb APIで使う server.port と、gRPCサーバーで使う spring.grpc.server.port は別です。

server:
  port: 8080

spring:
  grpc:
    server:
      port: 9090

この場合、REST APIは 8080、gRPCは 9090 で待ち受けます。

RESTとgRPCを同じアプリで扱う場合、ここを混同しやすいので注意しましょう。

導入前に確認したいこと

Spring BootでgRPCを導入する前に、次の点を確認しておくとスムーズです。

・Spring Boot 4.1系を使っているか
・gRPCを外部公開APIに使うのか、内部通信に使うのか
・.proto ファイルの管理方針を決めているか
・サーバー側とクライアント側の生成コードをどう共有するか
・ローカルではplaintext、本番ではTLSを使う方針か
・認証・認可をどうするか
・REST APIとの使い分けをチームで合意しているか
・テストで @AutoConfigureTestGrpcTransport を使うか

gRPCは便利ですが、すべてをgRPCに置き換える必要はありません。

外部公開APIはREST、内部サービス間通信はgRPC、という使い分けから始めるのが現実的です。

まとめ

Spring Boot 4.1では、gRPCサーバー・クライアントを作るための公式サポートが追加され、Spring Bootらしい自動構成でgRPCを扱いやすくなりました。

この記事では、hello.proto を定義し、@GrpcService でサーバーを実装し、@ImportGrpcClients でクライアントStubを注入する流れを紹介しました。

重要なポイントは以下です。

・gRPCのAPI定義は .proto ファイルに書く
・Gradleでは protobuf プラグインでJavaコードを生成する
・サーバー側は @GrpcService を付けて実装する
・クライアント側は @ImportGrpcClients でStubをBeanとして取り込む
・Nettyベースでは gRPC は通常 9090 ポートで待ち受ける
・REST APIの server.port とは別に考える
・テストでは @AutoConfigureTestGrpcTransport が便利

REST APIは今後も外部公開APIとして重要です。
一方で、サービス間通信や低レイテンシな内部APIでは、gRPCが有力な選択肢になります。

まずはこの記事のようなシンプルな HelloService から試し、次に認証、ヘルスチェック、ストリーミング、Observabilityへ広げていくのがおすすめです。

是非フォローしてください

最新の情報をお伝えします

類似投稿