Rust Webアプリケーションにおけるasync-trait を用いた非同期サービスレイヤーインターフェースの定義
Emily Parker
Product Engineer · Leapcell

Rust Webアプリにおける堅牢な非同期サービスの構築
非同期プログラミングは、高性能でスケーラブルなWebアプリケーションを構築するための不可欠なパラダイムになりました。Rustエコシステムでは、async/awaitは、並行コードの記述方法に革命をもたらし、I/Oバウンドな操作を処理するための強力で人間工学的な方法を提供しています。しかし、非同期サービスレイヤーの再利用可能でテスト可能なインターフェースを定義するという点になると、私たちはしばしば、Rustの古典的な制限に遭遇します。それは、トレイトが、動的ディスパッチ(dyn Traitの使用)を強制されることなく、その戻り値の型がトレイト実装に依存する非同期関数を直接含められないということです。この課題は、明確なアーキテクチャ設計を妨げ、効果的な単体テストを困難にする可能性があります。このブログ記事では、async-traitクレートがいかにこの問題をエレガントに解決し、Rust Webアプリケーションで真に非同期に依存しないサービスインターフェースを定義することを可能にし、よりモジュール化され、保守可能で、テストしやすいコードベースにつながるかを掘り下げます。
async-traitの基礎の理解
async-traitの実用的な適用について詳しく説明する前に、その有用性を理解するための基本となるいくつかのコアコンセプトを明確にしましょう。
- 非同期プログラミング (
async/await): Rustにおけるasync/awaitは、同期的に見え、感じられる非同期コードを記述するための構文を提供します。async fnはFutureを返します。これは状態マシンであり、完了までポーリングできます。awaitは、待機中のFutureが解決されるまで、現在のasyncブロックの実行を一時停止します。 - トレイト: Rustにおけるトレイトは、共通の振る舞いを定義するための基本的なメカニズムです。これらは、型が実装しなければならないメソッドのセットを指定することを可能にし、ポリモーフィズムとジェネリックプログラミングを可能にします。
- トレイトオブジェクト (
dyn Trait): トレイトメソッドが自己参照型またはコンパイル時にサイズが不明な型(実装ごとに具体的な型が異なるFutureなど)を返す場合、私たちはしばしばトレイトオブジェクトに頼ります。dyn Traitは、実行時に適切な具体的な実装への呼び出しをディスパッチすることを可能にしますが、動的ディスパッチによるオーバーヘッドが発生し、asyncコンテキストでのスレッド間での安全な共有のためにSendおよびSyncバウンドが必要になります。 - トレイトにおける
async fnの問題: コアな問題は、トレイトにおけるasync fnが概念的にimpl Future<Output = T>を返すことです。このFutureの具体的な型がトレイトの特定のインプリメンターによって決定される場合、トレイト(静的)はその具体的な戻り値の型とそのサイズを知ることができず、トレイト定義内での直接使用を妨げます。Rustの型システムは、このようなサイズ不明な型をトレイト内で直接使用することを防ぐように設計されています。 async-traitクレート:async-traitクレートは、trait定義内のasync fn宣言を、使用可能な形式に変換するプロシージャルマクロです。これは実質的にasync fnをBoxFuture(BoxとPinでラップされたFuture)を返す通常のfnにデシュガーし、戻り値の型を一貫性がありサイズがわかるものにし、トレイトシステムを満たします。これにより、トレイトオブジェクトが必要な場合をサポートしながら、トレイト定義自体でdyn Traitを必要とせずに、トレイトでasyncメソッドを定義できます。
非同期サービスインターフェースの実装
async-traitがいかにクリーンで非同期なサービスレイヤーの設計を可能にするかを例示しましょう。ユーザー管理のためにデータベースと対話する必要がある典型的なWebアプリケーションシナリオを考えてみてください。
async-traitなし (課題):
// BoxFutureを手動で使用するか、async-traitを使用しないとコンパイルできません // trait UserRepository { // async fn find_user_by_id(&self, id: u64) -> Result<User, UserError>; // async fn create_user(&self, user: User) -> Result<(), UserError>; // }
コンパイラは、トレイト内のasync fnがまだ安定していないか、Futureの戻り値の型が不明であるといった苦情を言うでしょう。BoxFutureを手動で使うことは冗長で繰り返しになります。
async-traitあり (解決策):
まず、Cargo.tomlにasync-traitを追加します。
[dependencies] async-trait = "0.1" tokio = { version = "1", features = ["full"] } # 例のランタイム
次に、サービスインターフェースを定義できます。
use async_trait::async_trait; use tokio::sync::Mutex; // 例のインメモリストア用 use std::collections::HashMap; use std::sync::Arc; // UserとUserError型を定義してください #[derive(Debug, Clone, PartialEq, Eq)] pub struct User { pub id: u64, pub name: String, pub email: String, } #[derive(Debug, thiserror::Error)] pub enum UserError { #[error("User not found")] NotFound, #[error("User with ID {0} already exists")] AlreadyExists(u64), #[error("Database error: {0}")] DatabaseError(String), } #[async_trait] pub trait UserRepository: Send + Sync { async fn find_user_by_id(&self, id: u64) -> Result<User, UserError>; async fn create_user(&self, user: User) -> Result<(), UserError>; }
トレイト定義の上の#[async_trait]属性に注目してください。このマクロは、トレイト内でasync fnを機能させる魔法です。Send + Syncバウンドは、デシュガーされたメソッドから返されるFutureがasyncアプリケーションで一般的な、スレッド間での移動と共有が安全でなければならいため、ここに不可欠です。
トレイトの実装 (例: インメモリリポジトリ):
インメモリハッシュマップを使用した具体的な実装を作成しましょう。
pub struct InMemoryUserRepository { store: Arc<Mutex<HashMap<u64, User>>>, } impl InMemoryUserRepository { pub fn new() -> Self { Self { store: Arc::new(Mutex::new(HashMap::new())), } } } #[async_trait] impl UserRepository for InMemoryUserRepository { async fn find_user_by_id(&self, id: u64) -> Result<User, UserError> { let store = self.store.lock().await; // ミューテックスをロック store.get(&id).cloned().ok_or(UserError::NotFound) } async fn create_user(&self, user: User) -> Result<(), UserError> { let mut store = self.store.lock().await; // ミューテックスをロック if store.contains_key(&user.id) { return Err(UserError::AlreadyExists(user.id)); } store.insert(user.id, user); Ok(()) } }
Webハンドラでのサービスの利用 (例: Axum):
ここでは、Axum Webサーバーにこれを統合する方法を示し、トレイトオブジェクトを使用した依存性注入を実証します。
// axumとserdeが設定されていると仮定 use axum::{ extract::{Path, State}, http::StatusCode, response::IntoResponse, Json, Router, }; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] pub struct UserRequest { pub name: String, pub email: String, } impl From<UserRequest> for User { fn from(req: UserRequest) -> Self { User { id: rand::random(), // 簡単のため、ランダムなIDを生成 name: req.name, email: req.email, } } } pub type SharedUserRepository = Arc<dyn UserRepository>; // 利便性のための型エイリアス async fn get_user( Path(user_id): Path<u64>, State(repo): State<SharedUserRepository>, ) -> Result<Json<User>, StatusCode> { match repo.find_user_by_id(user_id).await { Ok(user) => Ok(Json(user)), Err(UserError::NotFound) => Err(StatusCode::NOT_FOUND), Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), } } async fn create_user( State(repo): State<SharedUserRepository>, Json(payload): Json<UserRequest>, ) -> Result<Json<User>, StatusCode> { let new_user: User = payload.into(); match repo.create_user(new_user.clone()).await { Ok(_) => Ok(Json(new_user)), Err(UserError::AlreadyExists(_)) => Err(StatusCode::CONFLICT), Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), } } #[tokio::main] async fn main() { let user_repo: SharedUserRepository = Arc::new(InMemoryUserRepository::new()); let app = Router::new() .route("/users/:id", axum::routing::get(get_user)) .route("/users", axum::routing::post(create_user)) .with_state(user_repo); let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap(); println!("Listening on http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap(); }
アプリケーションとメリット:
- モジュール性:
UserRepositoryトレイトは、ユーザー関連のデータ操作の契約を、具体的なストレージメカニズムから独立して明確に定義します。InMemoryUserRepositoryをPgUserRepository(Postgres)、MongoUserRepositoryなどに簡単に置き換えることができます。Webハンドラーを変更する必要はありません。 - テスト容易性:
InMemoryUserRepositoryがUserRepositoryトレイトを実装しているため、実際のデータベース接続を必要とせずに、Webハンドラーやビジネスロジックをテストするために使用できます。これにより、迅速で分離された単体テストが可能になります。 - クリーンアーキテクチャ: このパターンは、Webレイヤー、サービスレイヤー(トレイトで定義)、データアクセスレイヤー(トレイトの実装)の間で関心を分離し、クリーンなアーキテクチャ設計を促進します。
- 依存性注入:
Arc<dyn UserRepository>を使用することで、実行時にリポジトリの異なる実装を注入でき、アプリケーションコンポーネントを疎結合にします。
結論
async-traitクレートは、Rust Web開発者にとって不可欠なツールです。Rustの非同期ストーリーにおける重要なギャップを埋め、サービスレイヤーのための真に非同期に依存しないトレイトインターフェースの定義を可能にします。トレイト内で直接async fnを許可することにより、async-traitは、高度にモジュール化され、テスト可能で、保守性の高いWebアプリケーションを促進し、堅牢なアーキテクチャパターンを一貫して推進します。async-traitを使用することで、柔軟でスケーラブルなRustサービスを自信を持って構築できます。

