【ITニュース解説】Adding Cache to NestJS Services Made Easy
2025年09月12日に「Dev.to」が公開したITニュース「Adding Cache to NestJS Services Made Easy」について初心者にもわかりやすく解説しています。
ITニュース概要
NestJSでアプリのパフォーマンスを上げるキャッシュの実装方法を解説する。デコレーターやAOPを使い、ビジネスロジックから分離してシンプルにコードを書ける。これにより、コントローラーやサービスなど、どこでもキャッシュ機能が利用可能になる。
ITニュース解説
アプリケーションのパフォーマンスを向上させることは、ユーザー体験を改善し、システムの安定性を保つ上で非常に重要である。特に、データベースへの頻繁なアクセスはアプリケーションの速度低下やサーバーへの負担増大の主な原因となる場合がある。ここで有効なのが「キャッシュ」という技術だ。キャッシュとは、一度取得したデータを一時的に保存しておき、次回同じデータが要求された際に保存されたデータを素早く返す仕組みを指す。これにより、データベースへのアクセス回数を減らし、アプリケーションの応答速度を大幅に向上させることができる。しかし、キャッシュ機能を実装する際に、本来のデータの処理や計算といった「ビジネスロジック」と、キャッシュの管理といった付随的な処理が混ざり合ってしまうと、コードが読みにくくなり、保守が困難になるという課題がある。
NestJSは、Node.js上で効率的なサーバーサイドアプリケーションを構築するためのフレームワークである。NestJSにおいて、このキャッシュの課題を解決する一般的な方法の一つとして、「デコレーター」と「インターセプター」を組み合わせるアプローチがある。デコレーターは、クラスやメソッドに付加して、その振る舞いを変更したり、追加情報(メタデータ)を与えたりする機能である。例えば、@Cachedというデコレーターを使って、特定のメソッドがキャッシュの対象であることを示し、キャッシュキー(データを識別するための名前)や有効期限(TTL)といった設定を渡す。一方、インターセプターは、HTTPリクエストの処理フローの途中に独自のロジックを差し込むことができる機能だ。具体的には、CacheInterceptorというカスタムインターセプターを作成し、このインターセプターが@Cachedデコレーターで設定された情報を読み取る。そして、まずキャッシュに目的のデータがあるかを確認し、あればそれをすぐに返す。キャッシュにデータがなければ、本来のメソッド(データベースクエリなど)を実行し、その結果をキャッシュに保存してから返すという流れでキャッシュ処理を行う。この方法により、キャッシュに関するロジックが、HTTPコントローラーのビジネスロジックから分離され、コードの可読性が高まる。
しかし、このインターセプターを用いた方法は、HTTPリクエストを直接処理するコントローラーのメソッドに限定されるという制約がある。NestJSアプリケーションには、HTTPリクエストに直接関わらず、純粋なビジネスロジックを実行する「サービス」と呼ばれる部分も存在する。これらのサービスメソッドは、他のサービスやバックグラウンド処理から呼び出されることが多く、HTTPリクエストの文脈を持たないため、標準のインターセプターを適用できない。これが、より柔軟なキャッシュ実装を求める上での課題となる。
このインターセプターの制約を克服し、アプリケーションのどのメソッドにも横断的な処理を適用できるようにするのが、「AOP(Aspect-Oriented Programming:アスペクト指向プログラミング)」という考え方である。AOPは、キャッシュ、ロギング、セキュリティといった、アプリケーションの複数の部分にまたがって存在する共通の機能(これを「横断的関心事」と呼ぶ)を、本来のビジネスロジックから分離して独立した「アスペクト」として定義するプログラミングパラダイムだ。これにより、共通処理がコードのあちこちに散らばるのを防ぎ、一箇所で管理できるため、保守性と再利用性が大幅に向上する。NestJSでAOPを実現するために、@toss/nestjs-aopというパッケージが提供されており、これを使うとデコレーターをサービスメソッドを含むあらゆるメソッドに適用できるようになる。
AOPを用いたキャッシュの実装はいくつかのステップで進められる。まず、@toss/nestjs-aopパッケージをインストールし、NestJSのメインモジュールにAopModuleをインポートしてAOP機能を使えるようにする。次に、キャッシュデコレーターを識別するためのユニークなシンボルを定義する。そして、キャッシュの具体的なロジックを担うCacheDecoratorクラスを作成する。このクラスはLazyDecoratorインターフェースを実装し、そのwrapメソッドの中に、キャッシュの確認、元のメソッドの実行、結果のキャッシュ保存といった処理を記述する。このCacheDecoratorは、キャッシュの読み書きを行うCacheServiceを利用する。CacheServiceは、get(取得)、set(保存)、delete(削除)といった基本的なキャッシュ操作を提供するサービスである。CacheDecoratorクラスはNestJSの依存性注入(DI)システムにプロバイダーとして登録される。最後に、実際にメソッドに付加するCachedデコレーター関数を作成する。この関数は、内部的に@toss/nestjs-aopのcreateDecorator関数を使い、定義したシンボルとキャッシュオプションを結びつける。これらの設定が完了すれば、HTTPコントローラーだけでなく、サービスメソッドに対しても@Cachedデコレーターを付加するだけで、簡単にキャッシュ機能を適用できるようになり、ビジネスロジックからキャッシュ処理を完全に分離できる。
このキャッシュ機能が正しく動作するためには、実際にデータを保存するキャッシュ基盤が必要である。記事では、@nestjs/cache-managerという公式パッケージと、高速なインメモリデータストアであるRedisを組み合わせた設定例が示されている。アプリケーション全体で利用できるように@Global()デコレーターが付与されたキャッシュモジュール内で、@nestjs/cache-managerのNestCacheModuleが非同期に登録される。この設定では、環境変数からRedisの接続情報を取得し、Keyvライブラリを使ってRedisへの接続を確立する。前述のCacheServiceは、このNestCacheModuleが提供するCacheインターフェースを内部で利用し、AOPのCacheDecoratorもこのCacheServiceを通じて実際のキャッシュ操作を行うため、CacheDecoratorもキャッシュモジュールのプロバイダーとして登録される。これにより、アプリケーションのどこからでも利用可能な共通のキャッシュ基盤が構築される。
AOPの魅力はキャッシュだけでなく、他の様々な横断的関心事にも適用できる点にある。記事では、ロギングとバリデーションの例を通じてその応用力を示している。ロギングはアプリケーションの動作状況を記録し、デバッグや監視に不可欠な処理である。AOPを使えば、@Loggedデコレーターをメソッドに付加するだけで、メソッドの呼び出し時、完了時、エラー発生時などに自動的にログを出力できる。ログレベルや引数、結果を含めるかどうかもオプションで設定可能だ。これにより、各メソッドでログ出力のコードを書く手間が省け、ログ出力ルールを一元的に管理できる。同様に、バリデーション(入力値の検証)もAOPで実現可能である。@Validatedデコレーターを使い、Zodなどのスキーマ定義ライブラリと連携させることで、メソッドの入力引数や返される結果が定義されたスキーマに合致しているかを自動的に検証できる。検証に失敗した場合は適切なエラーを返すように設定することで、各メソッドでの手動バリデーションロジックが不要となり、入力の安全性を効率的に確保できる。これらの例は、ビジネスロジックから付随的な処理を分離し、コードの再利用性と保守性を高めるAOPの強力なメリットを示している。
このように、NestJSでアスペクト指向プログラミングを活用することは、アプリケーション開発において多くの利点をもたらす。まず、ビジネスロジックが本質的な処理に集中できるため、コードの「クリーンな分離」が実現する。次に、一度定義したデコレーターとアスペクトは、コントローラー、サービス、その他のクラスのどのメソッドにも適用できるため、「再利用性」が大幅に向上する。さらに、複数のデコレーターを組み合わせて一つのメソッドに適用できるため、機能の「コンポーザビリティ(組み合わせやすさ)」が高まる。最後に、横断的ロジックの変更が必要になった場合でも、その変更は特定のアスペクトに限定されるため、「保守性」が向上し、アプリケーション全体への影響を最小限に抑えられる。NestJSの標準的なインターセプターはHTTP関連のシナリオには適しているが、AOPデコレーターは、アプリケーション内のあらゆるメソッドを強化するための柔軟で強力な手段を提供し、アプリケーションが成長するにつれてコードの清潔さと懸念事項の適切な分離を維持する上で非常に有効なアプローチである。