NestJSでgRPCクライアントを管理するtips

こんにちは、マネーフォワードケッサイでバックエンドの開発をしているgarsueです。

1年くらい前にNestJSでBFFをつくった話を書き、今も引き続きNestJSは活躍してくれています。

その間に得られた知見もいくつかあるので、その中からgRPCを呼び出す際のgRPCクライアントの管理方法についてのtipsを書こうと思います。

gRPCクライアントの初期化における注意点

公式ドキュメントでgRPCクライアントのインスタンス管理方法がいくつか紹介されています。

ほとんどの場合、上記の情報で特に困ることはないですが、gRPCクライアントを利用する箇所が増え、ある程度複雑化してきた場合に困ることがあります。

公式ドキュメントで紹介されている方法はいずれもInjectのタイミングで.protoファイルを読み込み、gRPCクライアントを初期化されます。

実はこのgRPCクライアントの初期化コストはそれなりに高いです。 いろいろなモジュールからgRPCクライアントを使おうとしてInjectしまくっていると、どんどんNestJS内のモジュール依存解決時間が伸び、サーバーの起動完了までの時間も伸びていきます。

特に巨大な.protoファイルを利用している場合に顕著で、あっという間に分単位で起動に時間がかかるような状況になってしまいます、というかなってしまいました。

このgRPCクライアントの初期化コストは開発中のリロード時間にもそのまま乗っかってくるので、このままだと開発効率もガタ落ちしてしまいます。

解決策

gRPCクライアントに対するラッパーのようなモジュールを1つ定義し、各モジュールはそのラッパーモジュール経由でgRPCクライアントを使うようにすれば解決します。 単純ですね。

前述の公式ドキュメント中でもgRPCサーバーで定義されているサービスをそのままNestJSのサービスにしたようなプロキシオブジェクトを作っています。 おそらくそのプロキシオブジェクトを各モジュールから利用する想定なのでしょうが、そういったプロキシオブジェクトを自動生成できるわけでもないので、gRPCの定義が巨大だと現実的ではないでしょう。

落とし所としてgRPCクライアントを保持して、ほぼそのままExportするような形でラッパーモジュールを実装しています。

以下のようなイメージです。

// モジュール定義
@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'HERO_PACKAGE',
        transport: Transport.GRPC,
        options: {
          package: 'hero',
          protoPath: join(__dirname, 'hero/hero.proto'),
        },
      },
    ]),
  ],
  providers: [MyGrpcService],
  exports: [MyGrpcService],
})
export class MyGrpcModule {}

// gRPCクライアントを保持するラッパーサービス
@Injectable()
export class MyGrpcService {
  public readonly heroService: HeroesService

  constructor(
    @Inject('HERO_PACKAGE')
    client: ClientGrpc,
  ) {
    this.heroesService = this.client.getService<HeroesService>('HeroesService');
  }
}

こういったときに気軽にクライアントを保持し続けてもNode.jsは安全なので、実装する上では楽でいいですね。

おわりに

まだまだNestJS周りの小ネタはありますが、今回はこんなところで。

NestJSに興味がある方はぜひお気軽にお声がけいただけたら幸いです。