Node.jsにおけるプロパティドリルの公式な代替手段、AsyncLocalStorageの解明
Emily Parker
Product Engineer · Leapcell

はじめに
Node.jsの非同期世界では、複数の関数呼び出しや非同期操作にわたるコンテキストの管理は、しばしば大きな課題となります。開発者は、中間レイヤーがそのデータを使用しない場合でも、コンポーネントや関数の多くのレイヤーを通じてデータを渡す必要がある「プロパティ・ドリル」というパターンによく遭遇します。この手法は、冗長で、密結合しており、保守が困難なコードベースにつながる可能性があります。ユーザーIDやリクエストコンテキストが、データベースクエリ、API呼び出し、その他の非同期タスクにまたがる可能性のあるコールスタックの奥深くからアクセス可能である必要があると想像してください。この情報を各関数シグネチャに明示的に渡すことは、すぐに煩雑でエラーが発生しやすくなります。この問題こそ、Node.jsエコシステムが解決しようとしてきた一般的なペインポイントを浮き彫りにしています。幸いなことに、Node.jsは堅牢で公式なソリューションを提供しています:AsyncLocalStorageです。この記事では、AsyncLocalStorageがプロパティ・ドリルのエレガントな代替手段をどのように提供し、Node.jsアプリケーションのコンテキスト管理を簡素化し、その明瞭性と保守性を向上させるかを掘り下げていきます。
AsyncLocalStorageの詳細
AsyncLocalStorageの複雑さを探る前に、その機能の基盤となるいくつかのコアコンセプトを明確にしましょう。
コア用語
- コンテキスト: プログラミングにおいて、コンテキストとは、特定の時点でコードの一部で利用可能な変数、値、状態のセットを指します。
AsyncLocalStorageの文脈では、明示的に渡されることなく、特定の非同期フロー内でグローバルにアクセス可能にする必要があるデータを特に指します。 - 非同期操作: これは、結果を待っている間に実行スレッドをブロックしない操作です。例としては、ファイルI/O、ネットワークリクエスト、タイマーなどがあります。Node.jsは本質的に非同期的であるため、これらの操作全体でのコンテキスト管理が重要です。
- イベントループ: Node.jsイベントループは、非同期コールバックを処理する基本的なメカニズムです。イベントを常にチェックし、関連するコールバックを実行し、必要に応じてコールスタックにタスクをディスパッチします。
- プロパティ・ドリル: 前述のように、これは、ネストされたコンポーネントや関数にデータを利用可能にするためだけに、直接必要としない複数のコンポーネントまたは関数のレイヤーを通じてデータを渡すアンチパターンです。
プロパティ・ドリルの問題点
Webサーバーで、そのリクエストの処理中に生成されるすべてのログメッセージに対してリクエストIDをログに記録したいシナリオを考えてみましょう。
AsyncLocalStorageなし(プロパティ・ドリル):
// logger.js function log(level, message, requestId) { console.log(`[${new Date().toISOString()}] [${level}] [RequestID: ${requestId}] ${message}`); } // service.js function getUserData(userId, requestId) { log('INFO', `Fetching user data for ${userId}`, requestId); // 非同期操作をシミュレート return new Promise(resolve => setTimeout(() => { log('DEBUG', `User data fetched for ${userId}`, requestId); resolve({ id: userId, name: 'John Doe' }); }, 100)); } // controller.js async function handleUserRequest(req, res) { const requestId = req.headers['x-request-id'] || 'N/A'; log('INFO', `Handling user request`, requestId); try { const user = await getUserData(req.params.id, requestId); log('INFO', `User data retrieved successfully`, requestId); res.json(user); } catch (error) { log('ERROR', `Error handling user request: ${error.message}`, requestId); res.status(500).send('Internal Server Error'); } } // app.js (スニペット) // app.get('/users/:id', handleUserRequest);
この例では、requestIdはhandleUserRequestからgetUserData、そしてlogに明示的に渡されています。getUserDataが別の関数を呼び出した場合、requestIdを再度渡す必要があり、プロパティ・ドリルにつながります。
AsyncLocalStorageの仕組み
AsyncLocalStorageは、非同期実行コンテキストにローカルなデータを格納する方法を提供します。これは、AsyncLocalStorageを使用して値を設定すると、その値は、setTimeout、Promise.then、awaitなど、いくつの非同期ジャンプが発生しても、同じ実行フローから開始されたすべての後続の非同期操作全体でアクセス可能になることを意味します。
これは、非同期操作を追跡するためのNode.jsの内部メカニズムを活用することで実現されます。asyncLocalStorage.run()を使用して新しい実行コンテキストに入ると、その実行ブロック内で設定した値は、そのブロック内から開始されたすべての後続の非同期タスクに自動的に関連付けられます。これらのタスクが最終的に実行されると、AsyncLocalStorageは正しいコンテキストが復元されることを保証します。これは、Node.jsのシングルスレッド、イベント駆動型の性質に適応された、マルチスレッド環境におけるスレッドローカルストレージに概念的に類似しています。
AsyncLocalStorageによる実装
前の例をAsyncLocalStorageを使用してリファクタリングしましょう。
const { AsyncLocalStorage } = require('async_hooks'); // AsyncLocalStorageインスタンスの初期化 const als = new AsyncLocalStorage(); // logger.js - これでロガーはrequestIdを引数として必要としなくなりました function log(level, message) { const store = als.getStore(); // 現在のストア(コンテキストを含む)を取得 const requestId = store ? store.requestId : 'N/A'; console.log(`[${new Date().toISOString()}] [${level}] [RequestID: ${requestId}] ${message}`); } // service.js - requestId引数はもうありません function getUserData(userId) { log('INFO', `Fetching user data for ${userId}`); return new Promise(resolve => setTimeout(() => { log('DEBUG', `User data fetched for ${userId}`); resolve({ id: userId, name: 'John Doe' }); }, 100)); } // controller.js - コンテキストを確立するためにals.run()を使用するようになりました async function handleUserRequest(req, res) { const requestId = req.headers['x-request-id'] || `REQ-${Date.now()}`; // 存在しない場合は一意のIDを生成 // AsyncLocalStorageコンテキスト内でリクエスト処理全体を実行します als.run({ requestId }, async () => { log('INFO', `Handling user request`); try { const user = await getUserData(req.params.id); log('INFO', `User data retrieved successfully`); res.json(user); } catch (error) { log('ERROR', `Error handling user request: ${error.message}`); res.status(500).send('Internal Server Error'); } }); } // app.js - Expressアプリでの使用例 const express = require('express'); const app = express(); app.get('/users/:id', handleUserRequest); const PORT = 3000; app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); });
この改訂された例では:
AsyncLocalStorageインスタンスを作成します:const als = new AsyncLocalStorage();。handleUserRequestで、requestIdを下位に渡す代わりに、als.run({ requestId }, async () => { ... });を使用して新しい非同期コンテキストを開始します。最初の引数は、このコンテキスト内でアクセス可能になるオブジェクト(store)です。async () => { ... }ブロック内で直接的または間接的に呼び出されるすべての関数は、als.getStore()を使用してこのコンテキストを取得できます。log関数は、requestIdを引数として必要としなくなり、als.getStore()から直接requestIdを取得するようになりました。
これにより、関数シグネチャが大幅にクリーンアップされ、コードがよりモジュール化されます。コンテキスト(requestIdなど)は、明示的な配線なしに、必要とされる場所と時間で暗黙的に利用可能になります。
一般的なアプリケーションシナリオ
AsyncLocalStorageは、非同期操作全体でコンテキスト情報を伝達する必要があるシナリオで広く役立ちます:
- リクエストトレーシング/ロギング: 示されているように、特定のリクエストのすべてのログメッセージにリクエストIDを関連付ける。
- 認証/認可: 明示的に渡すことなく、さまざまなサービスやデータアクセスレイヤーでアクセス可能にする必要があるユーザー情報(例:ユーザーID、ロール)を格納する。
- データベーストランザクション: 特定のフロー内のすべてのデータベース操作が同じトランザクションの一部であることを保証する、トランザクションコンテキストの管理。
- マルチテナンシー: 異なるテナントに対してデータアクセスのスコープが正しく設定されていることを保証するために、現在のテナントIDを格納する。
- パフォーマンス監視: リクエストフローに関連する開始時刻または特定のメトリックの記録。
考慮事項とベストプラクティス
- 過度の使用: 強力ですが、すべてのデータに
AsyncLocalStorageを使用することは避けてください。非同期フロー内の真にグローバルな「クロス・カッティング」の懸念事項には、最も適しています。ローカライズされたデータの正当なパラメータ渡しを置き換えないでください。 - ストアの不変性:
als.run()に渡されたオブジェクトは、als.getStore()から直接アクセスできます。このオブジェクトのプロパティを直接変更した場合(例:als.getStore().someProp = 'newValue')、その変更はそのコンテキスト内でグローバルに反映されます。複雑な状態の場合は、不変データを格納するか、変更が必要で副作用を避けたい場合はストアをクローンすることを検討してください。 - エラー処理:
als.run()ブロック内でエラーが発生した場合でも、コンテキストは正常にクリーンアップされます。ただし、als.run()外でエラーが発生する可能性のある同期部分を適切に処理するようにしてください。 - スコープ:
als.run()によって確立されたコンテキストは、そのrun呼び出しから開始された非同期実行フローに厳密に結び付けられています。無関係な非同期タスクや新しいトップレベル実行コンテキストでは、魔法のように利用可能になるわけではありません。
結論
AsyncLocalStorageは、Node.jsにおける非同期操作全体でコンテキストを効果的に管理するための、重要かつ公式なソリューションとして位置づけられています。データを伝達するためのクリーンで暗黙的なメカニズムを提供することで、煩雑なプロパティ・ドリルの必要性を効果的に排除し、より保守可能で、読みやすく、エラーの少ないコードにつながります。コンテキスト管理を一元化することにより、より堅牢でスケーラブルなアプリケーションを開発者に提供し、最終的にはNode.jsの非同期環境におけるより良いアーキテクチャパターンを促進します。コンテキスト処理のためのよりクリーンなアプローチを解き放つためにAsyncLocalStorageを採用してください。これは、明示的なコンテキスト渡しの複雑さからの真のパラダイムシフトです。

