0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SpringBoot×gRPCを用いたアプリケーションを作成してみる

0
Last updated at Posted at 2025-11-01

はじめに

  • 今回は、SpringBootアプリケーションでgRPCを用いたアプリケーションを開発する方法について記事化しようと思います

gRPCとは?

  • そもそも、gRPCとは?
    • gRPC(gRPC Remote Procedure Call)は、Googleが開発したオープンソースのRPCフレームワークです。HTTP/2をベースとした通信プロトコルで、Protocol Buffers (protobuf) を使用したデータのシリアライズを行います
  • REST APIと比較した時のメリットは?
    • 高速な通信
      • バイナリ形式のデータ転送でJSONより軽量
      • HTTP/2の仕組みにより、1つの接続で複数のリクエストを並行処理
    • 型安全性
    • .protoファイルでスキーマを定義し、コードを自動生成
    • コンパイル時に型チェックが行われ、実行時エラーを削減
    • 双方向ストリーミング
      • 4種類の通信パターンをサポート:
        • Unary(単一リクエスト/レスポンス)
        • Server streaming(サーバーからのストリーム)
        • Client streaming(クライアントからのストリーム)
        • Bidirectional streaming(双方向ストリーム)
    • マイクロサービスに最適
      • サービス間の効率的な通信
      • 言語に依存しない設計(例:JavaのサービスがGoのサービスを呼び出せる)
      • デッドライン、キャンセル、タイムアウトなどの機能を標準サポート
  • gRPCを採用する要件についてはどうか?
    • 超高頻度・大量のサービス間通信があるケース
      • 秒間数千〜数万リクエストレベルがありネットワーク帯域やレイテンシがボトルネックになっているケース
    • リアルタイムの双方向ストリーミングが必須なケース
      • チャット、リアルタイムダッシュボード、ゲームサーバーなどで、かつWebSocketでは実現が難しいケース
    • 言語間の強い型安全性が必須なケース
  • 結論、一般的なWebアプリケーションではREST APIで十分ではありそうです...

SpringBootアプリケーションでgRPCを利用する方法

アーキテクチャ

  • 今回作成するアプリケーションの概要は下記になります
  • 今回はWebFluxアプリケーションとgRPCサーバーの部分を作成します
    • Spring MVCを用いても実装可能ですが、今回はSpring WebFluxを使用します
[HTTPクライアント]
    ↓ HTTP (8080)
[WebFluxアプリケーション](gRPCクライアント)
    ↓ gRPC (9090)
[gRPCサーバー] ※Springアプリケーション自体は8081portで起動
  • また、プロジェクト構成(抜粋)は以下のようになります
spring-webflux-demo/
├── build.gradle                      # ビルド設定(gRPC依存関係、protobuf設定)
├── src/main/
│   ├── proto/
│   │   └── user.proto                # Protocol Buffers定義ファイル
│   ├── resources/
│   │   ├── application.yml           # WebFluxアプリ設定(ポート8080)
│   │   └── application-grpcserver.yml # gRPCサーバー設定(ポート8081)
│   └── java/com/example/
│       ├── springwebfluxdemo/         # WebFluxアプリケーション(HTTPサーバー)
│       │   ├── SpringWebfluxDemoApplication.java  # メインクラス
│       │   ├── config/
│       │   │   ├── GrpcClientConfig.java         # gRPCクライアント設定
│       │   └── service/
│       │       └── RemoteUserService.java        # gRPCクライアント呼び出し
│       │     ※※RemoteUserServiceの呼び出し部分(Controller等)の実装は割愛
│       └── grpcserver/                 # gRPCサーバーアプリケーション
│           ├── GrpcServerApplication.java        # メインクラス
│           ├── config/
│           │   └── GrpcServerConfig.java         # gRPCサーバー設定
│           ├── grpc/
│           │   └── UserGrpcService.java          # gRPCサービス実装
│           ├── service/
│           │   └── UserService.java              # ビジネスロジック
│           └── model/
│               └── User.java
└── build/generated/source/proto/main/ # Protocol Buffersから自動生成されるコード
    ├── java/                          # protobufメッセージクラス
    ├── grpc/                          # 標準gRPCスタブ
    └── reactor/                       # Reactor対応gRPCスタブ

依存関係の追加

implementation "io.grpc:grpc-netty-shaded:${grpcVersion}"
implementation "io.grpc:grpc-protobuf:${grpcVersion}"
implementation "io.grpc:grpc-stub:${grpcVersion}"
  • Protocol Bufferのユーティリティ機能を提供するライブラリ。Spring WebFluxでREST APIとgRPCを併用する等の要件がある場合に追加します
implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}"
  • Spring WebFluxを用いる場合は以下の依存関係も追加します(参考:https://github.com/salesforce/reactive-grpc)
    • ※ストリーミングや並列処理を行いたい場合は、Spring WebFluxを用いたほうがいいですが、そのような要件がないアプリケーションであればSpring MVCで十分です
implementation "com.salesforce.servicelibs:reactor-grpc-stub:${reactorGrpcVersion}"
  • また、Protocol Buffers(protobuf)形式の定義からgRPC通信を行うコードを生成するため、以下の記載を追加します
    • ※こちらも上述のgrpc-javaのGitHubページに記載があります
protobuf {
	protoc {
		artifact = "com.google.protobuf:protoc:${protobufVersion}"
	}
	plugins {
		grpc {
			artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
		}
        // Reactor対応スタブ(ReactorUserServiceGrpc)を追加で生成するための記載
		reactor {
			artifact = "com.salesforce.servicelibs:reactor-grpc:${reactorGrpcVersion}"
		}
	}
	generateProtoTasks {
		all().each { task ->
			task.plugins {
				grpc {}
				reactor {}
			}
		}
	}
}

protoの実装

  • Protocol Buffersのスキーマ定義ファイルである.protoファイルを作成します
  • serviceでgRPCのAPI仕様を作成し、messageでデータ構造(型)を定義します
    • Protocol Buffersにおけるid = 1の1という数字は「フィールド番号(field number)」と呼ばれ、Protocol Buffersはフィールド名ではなくこのフィールド番号を使用してデータをシリアライズ/デシリアライズします。これにより、JSONなどのテキストベースのフォーマットと比べて、より効率的なデータ転送を可能にしています
// .protoファイルはクライアントとサーバーで共有する
syntax = "proto3";

// パッケージ名の定義
package com.example.grpc.service;

option java_multiple_files = true; // 生成されるJavaファイルを分割(推奨)
option java_package = "com.example.grpc.service"; // 生成されるJavaコードのパッケージ名(推奨)

// ユーザーサービス定義
service UserService {
  // ユーザー情報を取得するRPC
  rpc GetUser (GetUserRequest) returns (GetUserResponse);
  
  // 新しいユーザーを作成するRPC
  rpc CreateUser (CreateUserRequest) returns (CreateUserResponse);
  
  // 全ユーザー情報をストリーミング形式で取得するRPC
  rpc GetAllUsers (GetAllUsersRequest) returns (stream GetAllUsersResponse);
}

// ユーザー情報の基本構造
message User {
  string id = 1;    // ユーザーID
  string name = 2;  // ユーザー名
  int32 age = 3;    // 年齢
}

message GetUserRequest {
  string id = 1;
}

message GetUserResponse {
  User user = 1;
}

message CreateUserRequest {
  string name = 1;
  int32 age = 2;
}

message CreateUserResponse {
  User user = 1;
}

message GetAllUsersRequest {
  int32 limit = 1;
}

message GetAllUsersResponse {
  User user = 1;
} 

gRPCサーバーの実装

  • GrpcServerConfig
    • gRPCサーバーの設定および起動を行います
    • REST APIでは意識しない部分(Spring Bootが自動でやってくれる)ですが、gRPCでは明示的に記述する必要があります
package com.example.grpcserver.config;

import java.io.IOException;
import java.net.InetSocketAddress;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.example.grpcserver.grpc.UserGrpcService;
import io.grpc.Server;
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;

@Configuration
public class GrpcServerConfig {

    @Value("${grpc.server.port:9090}")
    private int grpcPort;

    @Bean(destroyMethod = "shutdown")
    public Server grpcServer(UserGrpcService userGrpcService) throws IOException {
        Server server = NettyServerBuilder.forAddress(new InetSocketAddress("0.0.0.0", grpcPort))
                .addService(userGrpcService) // UserGrpcServiceに実装されたRPCメソッドをサーバーに登録
                .build();
        
        server.start(); // gRPCサーバーを起動
        return server;
    }
} 
  • UserGrpcService.java
    • protoでファイルから自動生成されたserviceクラスの抽象クラスを継承し、各serviceメソッドを実装します
    • レスポンスデータを、protoファイルから自動生成されたgRPCのレスポンス形式に変換します
      • ※使用しているリクエスト/レスポンスクラスは全てprotoから自動生成されたクラス
    • ここでは、UserGrpcService内でUserServiceをDIして、UserService内でビジネスロジックを実装しています。つまりUserGrpcServiceは、REST APIにおけるControllerと同様のレイヤー(プレゼンテーション層)に位置すると言えるでしょう
package com.example.grpcserver.grpc;

import com.example.grpc.service.*;
import com.example.grpcserver.model.User;
import com.example.grpcserver.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;

@Slf4j
@Component
@RequiredArgsConstructor
public class UserGrpcService extends ReactorUserServiceGrpc.UserServiceImplBase {

    private final UserService userService;
    private static final long TIMEOUT_MILLIS = 5000L;

    @Override
    public Mono<GetUserResponse> getUser(Mono<GetUserRequest> request) {
        return request.flatMap(req -> userService.getUserById(req.getId())
                .map(this::mapToGrpcResponse))
                .timeout(Duration.ofMillis(TIMEOUT_MILLIS))
                .doOnError(error -> log.error("ユーザー取得エラー: {}", error.getMessage()))
                .doOnSuccess(response -> log.info("ユーザー取得成功: {}", response.getUser().getId()));
    }

    @Override
    public Mono<CreateUserResponse> createUser(Mono<CreateUserRequest> request) {
        return request.flatMap(req -> {
                User newUser = new User();
                newUser.setName(req.getName());
                newUser.setAge(req.getAge());
                
                return userService.createUser(newUser)
                        .map(this::mapToGrpcCreateResponse);
            })
            .timeout(Duration.ofMillis(TIMEOUT_MILLIS))
            .doOnError(error -> log.error("ユーザー作成エラー: {}", error.getMessage()))
            .doOnSuccess(response -> log.info("ユーザー作成成功: {}", response.getUser().getId()));
    }

    @Override
    public Flux<GetAllUsersResponse> getAllUsers(Mono<GetAllUsersRequest> request) {
        return request.flatMapMany(req -> userService.getAllUsers(req.getLimit())
                .map(this::mapToGrpcStreamResponse))
                .timeout(Duration.ofMillis(TIMEOUT_MILLIS))
                .doOnError(error -> log.error("全ユーザー取得エラー: {}", error.getMessage()));
    }

    private GetUserResponse mapToGrpcResponse(User user) {
        return GetUserResponse.newBuilder()
                .setUser(mapUserToGrpc(user))
                .build();
    }

    private CreateUserResponse mapToGrpcCreateResponse(User user) {
        return CreateUserResponse.newBuilder()
                .setUser(mapUserToGrpc(user))
                .build();
    }

    private GetAllUsersResponse mapToGrpcStreamResponse(User user) {
        return GetAllUsersResponse.newBuilder()
                .setUser(mapUserToGrpc(user))
                .build();
    }

    private com.example.grpc.service.User mapUserToGrpc(User user) {
        return com.example.grpc.service.User.newBuilder()
                .setId(user.getId())
                .setName(user.getName())
                .setAge(user.getAge())
                .build();
    }
} 

gRPCクライアントの実装

  • GrpcClientConfig.java
    • userServiceChannelのBean定義を行い、gRPCサーバーへの接続チャネルを定義しています。今回はデモのため通信方式は平文を指定しています
    • userServiceStubのBean定義を行い、gRPCサーバーのメソッドを呼び出すクライアントを定義しています
      • 今回はWebfluxを用いているためReactorUserServiceGrpc.ReactorUserServiceStubを用いていますが、Spring MVCの場合は、protoファイルから自動生成されるUserServiceGrpc.UserServiceBlockingStub(同期処理・ブロッキング用),UserServiceGrpc.UserServiceStub(非同期処理・コールバック用)のいずれかを用いれば良いです
package com.example.springwebfluxdemo.config;

import com.example.grpc.service.ReactorUserServiceGrpc;
import io.grpc.ManagedChannel;
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GrpcClientConfig {

    @Value("${grpc.client.user-service.host:localhost}")
    private String userServiceHost;

    @Value("${grpc.client.user-service.port:9090}")
    private int userServicePort;

    @Bean
    public ManagedChannel userServiceChannel() {
        return NettyChannelBuilder
                .forAddress(userServiceHost, userServicePort)
                .usePlaintext() // 本番環境ではTLSを使用すること
                .build();
    }

    @Bean
    public ReactorUserServiceGrpc.ReactorUserServiceStub userServiceStub(ManagedChannel userServiceChannel) {
        return ReactorUserServiceGrpc.newReactorStub(userServiceChannel);
    }
}
  • RemoteUserService.java
    • protoでファイルから自動生成されたserviceクラスのStub(ReactorUserServiceGrpc.ReactorUserServiceStub)を利用し、各serviceメソッドを呼び出します
    • リクエストデータを、protoファイルから自動生成されたgRPCのリクエスト形式に変換します
package com.example.springwebfluxdemo.service;

import com.example.grpc.service.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;

@Service
@RequiredArgsConstructor
@Slf4j
public class RemoteUserService {

    private final ReactorUserServiceGrpc.ReactorUserServiceStub userServiceStub;
    private static final Duration TIMEOUT = Duration.ofSeconds(5);

    public Mono<User> getUserById(String userId) {
        return userServiceStub.getUser(
                GetUserRequest.newBuilder()
                        .setId(userId)
                        .build())
                .map(GetUserResponse::getUser)
                .timeout(TIMEOUT)
                .doOnError(error -> log.error("リモートユーザー取得エラー: {}", error.getMessage()))
                .onErrorResume(error -> {
                    log.info("ユーザー取得エラーを処理: {}", error.getMessage());
                    // 空のユーザーを返す代わりにエラーを伝播
                    return Mono.error(error);
                })
                .doOnSuccess(user -> log.info("リモートユーザー取得成功: {}", user.getId()));
    }

    public Mono<User> createUser(String name, int age) {
        return userServiceStub.createUser(
                CreateUserRequest.newBuilder()
                        .setName(name)
                        .setAge(age)
                        .build())
                .map(CreateUserResponse::getUser)
                .timeout(TIMEOUT)
                .doOnError(error -> log.error("リモートユーザー作成エラー: {}", error.getMessage()))
                .doOnSuccess(user -> log.info("リモートユーザー作成成功: {}", user.getId()));
    }

    public Flux<User> getAllUsers(int limit) {
        return userServiceStub.getAllUsers(
                GetAllUsersRequest.newBuilder()
                        .setLimit(limit)
                        .build())
                .map(GetAllUsersResponse::getUser)
                .timeout(TIMEOUT)
                .doOnError(error -> log.error("リモートユーザー一覧取得エラー: {}", error.getMessage()));
    }
} 
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?