Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Lessons Learned: CompletableFuture Security and Kafka Topic Grouping in Microservices

2025年09月23日に「Dev.to」が公開したITニュース「Lessons Learned: CompletableFuture Security and Kafka Topic Grouping in Microservices」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

非同期処理で認証エラー(401)が出たら、セキュリティ情報が別スレッドに伝わっていない可能性が高い。専用Executorで引き継ぐ。Kafkaでは、複数トピックに同じグループIDを使うとメッセージ処理が競合し、取りこぼしが起きる。各トピックにユニークなIDを与えよう。

ITニュース解説

システム開発の現場では、予期せぬ問題に直面することがよくある。ここでは、特にシステムエンジニアを目指す初心者が知っておくべき、非同期処理におけるセキュリティの落とし穴と、メッセージキューであるKafkaのコンシューマ設計に関する重要な学びについて解説する。これらは、現代のシステム開発において頻繁に遭遇する可能性のある、しかし見過ごされがちなポイントだ。

まず最初の問題は、複数のサービスが連携し合うマイクロサービスアーキテクチャで発生した、非同期処理とセキュリティコンテキストに関するものだ。Spring Bootというフレームワークを使って開発されたマイクロサービスで、他のサービスへの処理を非同期で行うためにCompletableFuture.supplyAsync()という機能を利用した。非同期処理とは、ある処理を実行している間にも、その処理が終わるのを待たずに別の処理を並行して進めることができる仕組みのことだ。これにより、システム全体の応答性を高めたり、ユーザーを待たせる時間を減らしたりすることが可能になる。

しかし、この非同期処理を行った結果、別のサービス(このケースではFeignクライアントというツールを使って呼び出されていた)への通信が「401 Unauthorized」というエラーで失敗する事態が起こった。このエラーは、認証されていない、つまり「お前は誰だ?ちゃんと身分を証明しろ」と拒否された状態を示すものだ。なぜこのようなことが起きたのだろうか。

原因は、CompletableFutureが新しいスレッド、つまり現在の処理とは全く別の、独立した作業場所で処理を実行することにあった。通常、Webアプリケーションでは、ユーザーがログインするとそのユーザーの身元情報や権限に関する情報(これを「セキュリティコンテキスト」と呼ぶ)が、そのユーザーのリクエストを処理するスレッドに紐付けられる。例えば、JWTトークンと呼ばれる認証情報が入ったチケットのようなものが、このセキュリティコンテキストの一部として扱われる。このチケットがあるからこそ、システムは「このユーザーは誰で、何ができるか」を判断できるのだ。

ところが、CompletableFutureで新しいスレッドが立ち上がると、デフォルトではこのセキュリティコンテキストが新しいスレッドに自動的に引き継がれないのだ。今回のケースでは、他のサービスを呼び出す際に、その身分証となるJWTトークンをHTTPリクエストの「Authorizationヘッダー」という部分に添付する必要があった。しかし、非同期処理のスレッドにはセキュリティコンテキストがなかったため、Feignクライアントの「インターセプター」という、リクエストを送信する前に情報を加工する仕組みが、このAuthorizationヘッダーを正しく追加できなかったのだ。結果として、身分証のないリクエストが送信され、連携先のサービスから「401 Unauthorized」と拒否されてしまったというわけだ。

この問題を解決するために取られた方法は、DelegatingSecurityContextExecutorServiceという特別な「エグゼキュータ(実行者)」で、非同期タスクを実行する元のエグゼキュータをラップすることだった。このDelegatingSecurityContextExecutorServiceは、非同期処理を実行する際に、現在のスレッドからセキュリティコンテキストをコピーし、それを新しい非同期スレッドに正しく伝播させる役割を担う。この変更により、新しいスレッドでもセキュリティコンテキストが利用可能になり、JWTトークンがAuthorizationヘッダーに正しく添付されるようになったため、401エラーは解消された。この経験から学ぶべきは、非同期処理を使う際には、セキュリティコンテキストが新しいスレッドに適切に引き継がれるか常に確認する必要があるということだ。

次に、メッセージキューシステムであるKafkaに関する問題を見てみよう。Kafkaは、大量のデータを効率的にやり取りするために使われるシステムで、Publish/Subscribeモデル(出版/購読モデル)を採用している。データを送る側(プロデューサ)は「トピック」と呼ばれる特定のカテゴリにメッセージを送り、データを受け取る側(コンシューマ)はそのトピックを購読してメッセージを受け取る。

問題は、複数のトピックからメッセージを受け取る際に、すべてのトピックに対して同じ「コンシューマグループID」を使っていたことだった。コンシューマグループIDは、複数のコンシューマでメッセージを分担して処理する際に重要な役割を果たす。同じグループIDを持つコンシューマたちは、あるトピックに送られてきたメッセージを協力して処理する。例えば、トピックAに100個のメッセージが来たら、同じグループIDのコンシューマが3人いれば、だいたい33個ずつメッセージを処理するといった具合だ。これにより、メッセージの処理を並行して行い、処理能力を向上させることができる。これを「負荷分散」と呼ぶ。

ところが、複数の異なるトピック(例えばトピックAとトピックB)に対して同じコンシューマグループIDを使ってしまった場合、予期せぬ動作が発生した。同じグループIDを持つコンシューマたちは、トピックAのメッセージもトピックBのメッセージも「自分たちのグループで処理すべきメッセージ」と認識してしまう。その結果、トピックAのメッセージを処理している間にトピックBのメッセージが見過ごされたり、あるいはその逆が起こったりして、メッセージの消費が不均衡になったり、最悪の場合メッセージが全く処理されない(スキップされる)といった問題に直面したのだ。

解決策はシンプルだが非常に効果的だった。それぞれのトピックに、ユニークな(固有の)コンシューマグループIDを割り当てるようにしたのだ。例えば、トピックAには「group-id-for-topic-A」、トピックBには「group-id-for-topic-B」といった具合だ。これにより、トピックAを購読するコンシューマグループと、トピックBを購読するコンシューマグループが完全に独立して動作するようになった。トピックAのメッセージは「group-id-for-topic-A」に属するコンシューマのみが処理し、トピックBのメッセージは「group-id-for-topic-B」に属するコンシューマのみが処理するようになる。こうすることで、各トピックのメッセージが独立して、確実に消費されるようになり、メッセージの見落としや消費の不均衡は解消された。

このKafkaの経験から得られる教訓は、コンシューマグループIDの設計は非常に重要であるということだ。同じグループIDはメッセージの負荷分散に役立つが、異なるトピックに対してはそれぞれ独立した消費を保証するために、通常は異なるグループIDを割り当てるべきだ。グループIDの設計を誤ると、メッセージングシステムの信頼性やパフォーマンスに大きな影響を与える可能性がある。

これらの二つの事例は、システム開発における細かいが致命的になりうる落とし穴を示している。非同期処理を用いる際にはセキュリティコンテキストの伝播を意識し、メッセージキューを使う際にはコンシューマグループIDの設計に細心の注意を払うことが、堅牢で信頼性の高いシステムを構築するための鍵となるだろう。システムエンジニアを目指す初心者は、このような実践的な学びを通して、より深い理解と問題解決能力を養っていくことが重要だ。

関連コンテンツ

関連IT用語