シングルトンパターンを用いたキャッシュ管理

シングルトンパターンを用いたキャッシュ管理

1. はじめに

企業向けサービスでは、ユーザーごとの権限管理は非常に重要な機能の一つです。ユーザーがログインした後、どのメニューを表示できるか、どの機能を使用できるか、どのデータにアクセスできるかは、すべて権限情報によって決まります。
開発中のサービスも、ユーザー権限に応じて表示されるメニューが変わる構造になっていました。管理者は専用の管理者ページから、ユーザーまたは役割(Role)ごとにメニュー権限を設定できます。ユーザーがログインすると、その権限情報に基づいてアクセス可能なメニューを構成し、画面に表示します。
このような権限管理機能はサービス運用において必須ですが、実装方法によってはシステムのパフォーマンスや保守性に大きな影響を与える可能性があります。特にユーザー数が増加し、ログインリクエストが多くなるほど、権限の取得方法について検討する必要があります。
本稿では、メニュー権限情報を管理する過程で発生した問題と、それを解決するためにシングルトンパターンを活用したインメモリキャッシュを導入した背景、実装方法、そして導入後に得られた効果について紹介します。

2. 既存の構造と問題点

初期システムは比較的単純な構造で設計されていました。
ユーザーがログインすると、権限取得サービスを呼び出します。サービスはデータベース(DB)から該当ユーザーのメニュー権限情報を取得します。その後、取得したデータに基づいてメニュー一覧を生成し、ユーザーに返す方式でした。
構造を簡単に表すと、次のようになります。
1. ユーザーログイン
2. 権限取得サービスの呼び出し
3. データベースの照会
4. メニュー権限情報の生成
5. ユーザー画面への返却
開発初期はユーザー数が多くなかったため、この方式でも十分に運用できました。データ量も大きくなく、ログインリクエストも多くなかったため、データベースの負荷が目立って発生することはありませんでした。
しかし、サービスの運用期間が長くなり、ユーザー数が増加するにつれて、いくつかの問題点が現れ始めました。

同一データの繰り返し取得

最初に発見した問題は、同じ権限情報を繰り返し取得しているという点でした。
例えば、一般ユーザーグループに属するユーザーが1,000人いるとします。これらのユーザーは全員、同じメニュー権限情報を使用します。しかし、ログインするたびにデータベースを照会する構造では、同じデータを1,000回繰り返し読み込むことになります。
権限情報はほとんどの場合変更されないにもかかわらず、毎回データベースへのアクセスが発生していたのです。

データベース負荷の増加

権限情報の取得は、ログイン処理で必ず実行される作業です。
したがって、ログインリクエストが増加するほど、データベースの照会回数も比例して増加します。特に講義の開始時刻やシステムメンテナンス後にユーザーが同時にログインする場合、権限取得SQLが集中的に実行されました。これはデータベース負荷増加の原因となる可能性がありました。
もちろん、1回の照会自体にかかるコストは大きくありません。しかし、数百人または数千人のユーザーが同じ時点でログインすると、不要な照会が積み重なり、システム全体のパフォーマンスに影響を与える可能性があります。

応答時間の増加

ユーザーがログインした後、最初の画面を表示するには権限情報の取得が完了している必要があります。
データベースの応答速度が低下したり、一時的な負荷が発生したりすると、ログインの応答時間にも直接的な影響が及びます。権限の取得はサービスロジック上必須の処理であるため、この部分のパフォーマンス改善はユーザー体験の向上においても重要な要素でした。

拡張性の限界

現在は大きな問題がなくても、サービス規模が大きくなるほどログインリクエストも増加します。
すべてのリクエストがデータベースを直接照会する構造は、長期的には拡張性の面で限界を持つ可能性があります。そのため、取得性能を改善しながら保守性も確保できる新しい構造が必要でした。

3. データ特性の分析

パフォーマンス改善策を検討するため、まずメニュー権限データの特性を分析しました。分析の結果、メニュー権限データには次のような特徴がありました。
第一に、データの変更頻度が非常に低いことです。権限情報は管理者ページからのみ変更できます。一般ユーザーがサービスを利用する過程では、ほとんど変更されません。
第二に、取得頻度が非常に高いことです。ユーザーがログインするたびに権限情報を取得する必要があります。一部の機能では、追加の権限検証も実行します。
第三に、同じデータを複数のユーザーが共有することです。同じ役割(Role)に属するユーザーは、ほとんどの場合、同じ権限情報を使用します。
つまり、メニュー権限データは「変更は少なく、取得は多い(Read-Heavy)データ」という特徴を持っていました。このようなデータは、キャッシュ適用の効果が最も大きい代表的なケースです。そのため、権限情報をデータベースではなくメモリで管理する方法を検討することにしました。

4. なぜシングルトンパターンを選択したのか

キャッシュを適用することを決定した後、もう一つ悩みがありました。それは、キャッシュオブジェクトをどのように管理するかということです。
権限情報はログインサービスだけでなく、メニュー生成サービスや権限検証サービスなど、さまざまなコンポーネントで使用されます。もしサービスごとに別々のキャッシュオブジェクトを生成すると、同じデータがメモリに複数回読み込まれる可能性があります。また、あるサービスでキャッシュを更新しても、他のサービスのキャッシュには反映されないため、データ不整合の問題が発生する可能性があります。
これを解決するため、アプリケーション全体で一つのキャッシュオブジェクトだけを使用するように設計しました。このとき適しているパターンが、シングルトン(Singleton)パターンです。シングルトンパターンは、アプリケーション内で特定のオブジェクトを一つだけ生成し、すべてのコンポーネントがそのオブジェクトを共有するデザインパターンです。シングルトンパターンを適用すると、次のようなメリットが得られます。

データの一貫性の確保

すべてのサービスが同じキャッシュオブジェクトを使用するため、権限データの一貫性を維持できます。

メモリ使用量の最小化

権限データを一度だけメモリに読み込むため、重複保存を防止できます。

高速なデータアクセス

データベースではなくメモリからデータを取得するため、応答速度が向上します。

保守性の向上

キャッシュ関連のロジックが一つのオブジェクトに集約されるため、管理しやすくなります。Spring Framework環境では、基本的にBean ScopeがSingletonであるため、このような構造を自然に実装できます。

5. 実装方法

実装は比較的単純な構造で進めました。
アプリケーションの起動時にデータベースからメニュー権限情報を取得してキャッシュに保存します。その後、ログインリクエストが発生すると、データベースを参照せずにキャッシュデータを返します。管理者が権限情報を変更するとキャッシュを更新し、最新の状態を維持します。
実装の流れは次のとおりです。
1. アプリケーションの起動
2. メニュー権限情報の取得
3. キャッシュへの保存
4. ログインリクエストの発生
5. キャッシュデータの返却
6. 管理者による変更時のキャッシュ更新
以下は概念を簡略化したサンプルコードです。

image1.png

上記のコードでは、権限情報を保存するキャッシュオブジェクトが1つだけ生成されます。
ログイン時にはgetMenus()メソッドを通じてキャッシュされたメニュー情報を取得し、管理者ページで権限情報が変更されるとrefresh()メソッドを呼び出して最新データを反映します。
実際の運用環境では、サービス層の分離、例外処理、初期ロード失敗への対応、モニタリングなどの機能を追加して使用しました。

6. 運用時に考慮した事項

キャッシュを適用する際に最も重要だと考えたのは、キャッシュの無効化(Cache Invalidation)でした。キャッシュを使用する最大のリスクは、実際のデータとキャッシュデータが異なる状態になることです。権限情報は頻繁に変更されるものではありませんが、変更の可能性がまったくないわけではありません。管理者が特定のメニュー権限を変更したにもかかわらずキャッシュが更新されなければ、ユーザーは変更前の権限を使い続けることになります。これを防ぐため、管理者ページで権限の変更が完了すると、直ちにキャッシュを再ロードするように実装しました。

また、同時実行性の問題についても考慮する必要がありました。権限情報の取得は多数のユーザーが同時に実行する可能性があるため、通常のHashMapではなくConcurrentHashMapを使用してスレッドセーフ性を確保しました。

現在の構成は、単一サーバー環境を前提として設計されています。単一インスタンスでは、アプリケーションメモリに保存されたキャッシュだけを管理すればよいため、構成は比較的シンプルです。しかし、今後サービス規模が拡大し、マルチインスタンス環境へ拡張する場合には、各サーバーが異なるキャッシュデータを保持する可能性があるという問題が発生します。

例えば、管理者が特定のメニュー権限を変更した際に、1台のサーバーのキャッシュだけが更新され、他のサーバーのキャッシュが更新されなければ、ユーザーごとに異なる権限情報が取得される可能性があります。この問題を防ぐためには、Redisのような分散キャッシュを導入するか、キャッシュ更新イベントを各サーバーへ伝播する構成を検討できます。現在は単一サーバー環境であるため適用していませんが、今後の拡張時に検討できる事項として整理しました。

アプリケーションの再起動時についても考慮しました。サーバーが再起動すると、メモリに保存されたキャッシュデータはすべて失われます。そのため、アプリケーションの起動時に権限情報を自動的にロードする初期化ロジックを実装し、サービスの可用性を維持しました。

このような経験を通じて、キャッシュは単に保存することよりも、「いつ更新するのか」を設計することがはるかに重要であると確認できました。

7. 導入結果

シングルトンパターンに基づくインメモリキャッシュを適用した後、さまざまな効果を得ることができました。
1つ目は、ログインプロセスで発生する権限取得SQLの実行回数が大幅に減少したことです。従来はログインのたびにデータベースを参照していましたが、キャッシュ適用後はほとんどのリクエストがメモリ上で処理されるようになりました。
2つ目は、データベースの負荷が減少したことです。権限取得に使用されていたコネクションの使用量が減少し、データベースリソースをより効率的に利用できるようになりました。
3つ目は、応答速度が改善したことです。メモリからの取得はデータベースからの取得よりもはるかに高速であるため、ログイン処理のパフォーマンスが向上しました。
4つ目は、保守性が向上したことです。権限情報が1つのオブジェクトで管理されるため、構成を理解しやすくなりました。障害分析や機能改善の際にも、関連するロジックを迅速に把握できるようになりました。
5つ目は、将来の拡張に向けた基盤を整備できたことです。現在はインメモリキャッシュを使用していますが、今後システム規模が拡大した場合には、Redisのような分散キャッシュソリューションへ移行できる構造的な基盤を確保しました。

8. おわりに

メニュー権限情報のように、変更頻度は低く参照頻度が高いデータは、キャッシュ適用の効果が非常に大きい領域です。ユーザーのログイン後に必要となるメニュー権限情報をシングルトンパターンに基づくインメモリキャッシュで管理することで、データベースの負荷を軽減し、応答性能を改善できました。
特に今回の適用を通じて、単にキャッシュを使用するよりも、データの特性を分析し、適切な設計パターンを選択することが重要であると改めて確認できました。シングルトンパターンは比較的シンプルな構造ですが、全体で共有されるデータを管理する必要がある状況では、非常に効果的な選択肢となり得ます。
今後もサービスの特性に適したキャッシュ戦略とアーキテクチャの改善を継続的に検討し、より安定的で効率的なシステムを構築していく予定です。

dwmoon

Site footer