NestJS の Injection scopes についてまとめました。

結論

@Injectable() デコレータになんとなくオプションをつけないようにしましょう。ほとんどのユースケースでオプションは不要です。

はじめに

Injection scopes とは、言い換えると Injection するクラス(provider)をどの頻度でインスタンス化するかの設定であり、具体的には Injectable デコレータのオプションです。

@Injectable() デコレータには、次の 2 つのオプションがあります。

  • scope : DEFAULT SCOPE TRANSIENT
  • durable : boolean

doc: https://docs.nestjs.com/fundamentals/injection-scopes

scope

まず scope オプションについて見てみましょう。

Provider scope

A provider can have any of the following scopes:
DEFAULT : A single instance of the provider is shared across the entire application. The instance lifetime is tied directly to the application lifecycle. Once the application has bootstrapped, all singleton providers have been instantiated. Singleton scope is used by default.
REQUEST : A new instance of the provider is created exclusively for each incoming request. The instance is garbage-collected after the request has completed processing.
TRANSIENT : Transient providers are not shared across consumers. Each consumer that injects a transient provider will receive a new, dedicated instance.

ざっくり要約すると、各設定は以下の通りです。

  • DEFAULT : シングルトン。アプリケーション初期化時のみ、インスタンス化する。
  • REQUEST : リクエスト単位。
  • TRANSIENT : 毎回。

パフォーマンス

scope オプションの設定によるパフォーマンスへの影響についてです。

Performance

Using request-scoped providers will have an impact on application performance. While Nest tries to cache as much metadata as possible, it will still have to create an instance of your class on each request. Hence, it will slow down your average response time and overall benchmarking result. Unless a provider must be request-scoped, it is strongly recommended that you use the default singleton scope.

意訳
メタデータとか頑張ってキャッシュするけど、リクエストごとにインスタンス化されると限界がある。当然パフォーマンスは落ちるよ。どうしても使わない場合を除いて、シングルトンスコープ(DEFAULT)を使ってね。

durable

scope よりちょっと複雑なオプションです。

まず、どのような場合に、リクエスト単位でインスタンス化する必要があるのか考えてみましょう。

例えば、マルチテナントなアプリケーションがあるとします。顧客が 10 社いて、それぞれのデータベースを分離させている場合を考えます。リクエストが来たら、Authorization ヘッダなどを取り出して、リクエストしたユーザの企業を特定します。

リクエストしたユーザの企業のデータベースに接続するように、DatabaseService を利用するとしましょう。リクエストごとに接続先のデータベースも変わるので、DatabaseService はシングルトンにすることができず、リクエスト単位でインスタンス化する必要があります。

リクエストを受け取る UserController が DatabaseService に依存している場合、依存先の DatabaseService がリクエスト単位でインスタンス化されるなら、当然 UserController もリクエスト単位でインスタンス化される必要があります。

こうなると、パフォーマンスが低下するのは明白です。

これを回避するのが durable オプションになります。今回の例だと、企業 ID ごとにインスタンスをキャッシュするようにすることで、パフォーマンスの低下を防ぎます。

例えば、以下のような挙動になります。

リクエストユーザ A(企業 1)→ インスタンス化
リクエストユーザ B(企業 1)→ 企業 1 のインスタンスがすでにあるので、それを使う
リクエストユーザ C(企業 2)→ インスタンス化

どのような方針でキャッシュするかについては、別途こんな感じの Strategy を書いて、NestJS に設定する必要があります

NestJSにおけるインスタンスのストラテジの画像

詳しく知りたい方はドキュメントをご参照ください
https://docs.nestjs.com/fundamentals/injection-scopes#durable-providers

まとめ

そもそもリクエスト単位でインスタンス化が必要になるのは、マルチテナントなアプリケーションを作る場合など、限られたユースケースになると思います。

逆に、マルチテナントなアプリケーションを作るときには、DI コンテナについてしっかり理解し、パフォーマンスとセキュリティを向上させることが重要だとわかりました。

多くのアプリケーションにおいてはまず必要のない設定なので、デフォルトの設定が一番パフォーマンスがいいことを理解しつつ、なんとなく @Injectable() デコレータにオプションを追加しないようにしたいです。