Axum

AxumのState抽出器と公式推奨パターン|DI・Arc活用ガイド

ⓘ本ページはプロモーションが含まれています

スポンサードリンク

Axum の State 抽出器の仕組みと公式が推奨する実装パターン

Axum ではリクエストハンドラに対して State 抽出器を使うことで、アプリ全体で共有するデータへ安全かつ効率的にアクセスできます。本セクションでは、State<T> が内部でどのように動作し、公式ドキュメントが示すベストプラクティスを具体例とともに解説します。

State 抽出器の基本構造

axum::extract::State<T>Router::with_state(state) で渡した値を内部的に Arc<T> に包み、リクエストごとにその Arc をクローンしてハンドラへ渡します。重要なのは、T が Clone を実装している必要はなく、Arc::clone によってポインタだけがコピーされる点です。したがって、状態オブジェクト自体に重い clone 実装を用意する必要はありません。

この仕組みにより、スレッド間で安全に共有でき、所有権の移動も発生しません。公式ドキュメントでも「State は内部で Arc::clone する」ことが明記されています。


公式推奨コード例(非同期環境に適した実装)

以下は Axum アプリケーションで State を利用する際の最小構成です。ミューテックスには標準ライブラリ版ではなく、非同期タスクがブロックされない tokio::sync::Mutex を使用しています。

ポイントまとめ

  • State<T> は内部で Arc::clone されるだけなので、TClone 制約は不要です。
  • 非同期ハンドラでは必ず tokio::sync::Mutex(あるいは RwLock)を使い、標準の std::sync::Mutex がタスクをブロックしないようにします。
  • Router::with_state(state) → ハンドラで State(state): State<T> の流れが公式推奨パターンです。

DI(依存性注入)としての State 活用シーン

Axum の State はアプリ全体で共有するコンテキストを提供できるため、DI コンテナ的に利用できます。このセクションでは、サービスごとにトレイトを定義し、実装とモックを切り替える手順を具体例で示します。

DI パターンの設計フロー

  1. 機能単位でトレイトを作成
  2. 例: データベースアクセス用に UserRepository トレイトを定義。
  3. 本番実装とテスト実装を分離
  4. 本番は SQLx のプール、テストはインメモリのフェイク実装。
  5. トレイトオブジェクト (Arc<dyn Trait + Send + Sync>) を AppState に格納
  6. これによりハンドラ側は具体型を意識せずに利用可能です。

テストコードでは AppState { user_repo: Arc::new(FakeUserRepo) } を渡すだけで、ハンドラは本番と同じインターフェイスを呼び出します。

ポイントまとめ

  • Arc<dyn Trait> により実装の差し替えが容易になり、テストの高速化やモックの注入がシンプルに。
  • DI の観点からは State = アプリ全体のコンテナ と位置づけると考えやすいです。

Arc と Mutex / RwLock を使ったスレッド安全な可変共有

マルチスレッド・非同期環境で状態を変更する場合、Arc<Mutex<T>> または Arc<RwLock<T>> が一般的です。ここではそれぞれの特徴と、async コンテキストでの正しい使い方 を解説します。

Arc\<Mutex> と Arc\<RwLock> の比較

特性 Arc<Mutex<T>> Arc<RwLock<T>>
同時アクセス形態 1 スレッドだけが排他ロック可能 複数の読み取りは同時に許可、書き込みは排他
書き込み頻度が高いケース シンプルでオーバーヘッドが小さい 読み取りが多く書き込みが少ないと有利
デッドロックリスク ロック順序さえ守れば低め 昇格(read → write)や二段階ロックで注意必要

Arc の役割
Arc が参照カウントを管理し、クローン時にポインタだけがコピーされます。したがって T 自体に Clone は不要です。

非同期環境でのミューテックス選択

  • tokio::sync::Mutex / RwLock
  • タスクをブロックせずに待機でき、スケジューラが他タスクへ切り替えます。
  • std::sync::Mutex(同期版)
  • 非同期ハンドラで使用すると、内部でスレッドをブロックしデッドロックやパフォーマンス低下の原因になります。

正しいロック取得例

ロック時間を最小化するテクニック

  1. スコープを限定let guard = ...; …; drop(guard); のように明示的に解放。
  2. 順序統一:複数ロックが必要な場合は常に同じ取得順序でデッドロック回避。
  3. 非同期ブロッキング回避:長時間かかる I/O はロック外で実行し、ロックは状態更新だけに限定。

実装サンプル:DB 接続プール・キャッシュ・設定情報をまとめた AppState

ここまでの概念を統合し、実際に動くコード例を示します。DB プール、キャッシュ、設定情報 を一つの AppState に集約し、ハンドラで安全に利用できる構成です。

構造体定義と初期化(導入文)

以下の実装では、データベースは SQLx の非同期プール、キャッシュは RwLock<HashMap<…>>、設定情報は不変な構造体として保持します。各フィールドはすべて Arc でラップし、ハンドラが自由にクローンできるようにしています。

ハンドラでの State 抽出と利用例(導入文)

次に示すハンドラは、キャッシュを先に確認し、ヒットが無ければ DB から取得してキャッシュへ書き戻すという典型的なパターンです。ロックは最小スコープで取得し、非同期 I/O はロック外で実行しています。

アプリ起動部(導入文)

main 関数では init_state() で作成した AppStateRouter::with_state に渡すだけです。これにより、全てのルートが同一インスタンスを共有します。

ポイントまとめ

  • Arc<Pool>Arc<RwLock<_>> の組み合わせで「読み取りは高速、書き込みは安全」に実装できる。
  • ロックの取得範囲を最小にし、非同期 I/O はロック外で行うことでデッドロックやスループット低下を防げます。

Extension 抽出器との違いと使い分けポイント

Axum には State<T> のほかに Extension\<T> という抽出器があります。どちらを選択すべきかは「可変性」「スコープ」「テスト容易性」によって決まります。このセクションでは比較表とチェックリストで判断基準を示します。

比較表(導入文)

以下の表は StateExtension の主な違いをまとめたものです。実際にプロジェクトでどちらを採用すべきか検討する際の参考にしてください。

観点 State<T> Extension<T>
所有権取得方法 Arc<T> が内部でクローンされるので、TClone 不要。ハンドラは所有権を取得しない形で利用。 任意の型をそのまま保持でき、Clone 制約は不要。ただし取得時に値自体が移動するか参照になるかは設計次第。
可変データの扱い Arc<Mutex<_>> / Arc<RwLock<_>> で包む必要がある。 同様に Arc 系を推奨。ただしミドルウェアレベルで状態を書き換えるケースは少ない。
スコープ アプリ全体 (Router::with_state) に対して一度設定すれば全ルートで共有。 router.layer(Extension::new(value)) でレイヤー単位に付与でき、サブルーティングごとに異なる値を持たせられる。
DI 表現力 アプリ全体のコンテナとして機能し、サービス群をまとめて管理しやすい。 個別サービスやリクエストスコープの情報(例: トレース ID)を付与する用途に向く。
テスト置換性 AppState 全体をモックに差し替えるだけで済む。 特定の Extension だけを差し替えられるため、粒度が細かいテストが可能。

選択指針チェックリスト(導入文)

プロジェクトの要件に合わせて以下の質問に答えてみてください。

  1. 全体で共有すべき状態はあるか
  2. DB プール、キャッシュ、設定情報などアプリ全体で同一インスタンスを使うなら State が自然。

  3. ルートごとに異なる設定が必要か

  4. 認証スキーマやテナント別のコンフィグは Extension のレイヤー化で柔軟に対応できる。

  5. 可変データへの同時書き込みが頻繁に起こるか

  6. 書き換えが必要な場合は State + Arc<Mutex> がシンプル。

  7. テストで特定サービスだけを差し替える必要があるか

  8. その場合は Extension を使うと、対象だけモックに置き換えて他は実装のままにできる。

  9. 所有権エラーが頻発しているか

  10. State は常に Arc::clone で済むので、型設計を見直すだけで解決できることが多い。

実務的なヒント:大規模プロジェクトでは「DB・キャッシュ・設定」は State に集約し、認証情報やリクエスト単位のトレース ID は Extension で付与するとコードベースがすっきりします。


まとめと次に取るべきアクション

本稿では以下の点を解説しました。

  • State の内部実装Arc::clone によって所有権は移動せず、TClone を要求されないこと。
  • 非同期環境でのミューテックス選択tokio::sync::MutexRwLock の使用が必須である理由。
  • DI パターンとしての State:トレイトオブジェクトと Arc による実装差し替え手法。
  • Arc と Mutex / RwLock の比較とベストプラクティス:ロック時間短縮・デッドロック回避策。
  • 実装サンプル:DB 接続プール、キャッシュ、設定情報をまとめた AppState とハンドラでの具体的な利用例。
  • Extension との比較と選択指針:スコープ・可変性・テスト容易性に基づく判断基準。

これらのパターンを自プロジェクトへ取り入れることで、安全かつテストしやすい状態共有が実現します。まずは以下のステップで導入を検討してください。

  1. 既存コードで std::sync::Mutex を使用している非同期ハンドラを tokio::sync::Mutex に置き換える。
  2. アプリ全体で共有するデータ構造を AppState に集約し、Router::with_state へ渡す。
  3. DI が必要なサービスはトレイトオブジェクト化し、テスト時にモック実装と差し替えるパイプラインを作る。
  4. 必要に応じて Extension を導入し、ルートごとのカスタム設定やリクエストスコープ情報を管理する。

サンプルコードは GitHub リポジトリ(https://github.com/yourname/axum-state-example)でも公開しています。ぜひフォークして自分のプロジェクトに合わせて拡張・改善し、実際に動かしてみてください。


参考文献

  • Axum 公式ドキュメント – State 抽出器
  • Tokio ドキュメント – tokio::sync::Mutex / RwLock
  • Zenn 記事「axum State の仕組み」(https://zenn.dev/sbk0716/articles/e1f1c0de4d68e9)

これらを併せて読むことで、さらに深い理解が得られます。 Happy coding!

スポンサードリンク

-Axum