『RustによるWebアプリケーション開発 設計からリリース・運用まで』を読んだ
目次
『RustによるWebアプリケーション開発 設計からリリース・運用まで』を読んだので感じたことをメモしておく。
モチベーション#
Rustという言語の開発体験がかなり好きでRustでサーバーサイドを書くのがどんな感じなのか純粋に気になっていたので読んでみた。(筆者がサーバーサイドの開発でガッツリ書いたことがあるのはほぼGoのみで、RustはCLIやTUIの開発にしか使ったことがないというバックグラウンド)
本書の概要#
本書はRustで蔵書管理アプリケーションのサーバーサイドを実装しながらRustでのWebアプリケーション開発の仕方を学ぶことができる本。
ただ動くものを作る、というよりは業務の実際のサーバーサイドの開発でよく出くわす課題をRustではどう実装するのかが豊富に紹介されている。(DIやエラーハンドリングなど)
設計面ではレイヤードアーキテクチャを採用しており、レイヤー間の責務などが考慮された設計になっている。その意味でも実務での開発に近い内容になっていると言える。
あとはステップごとのコード差分が丁寧に書かれているので、本書に従って実装を進めるとWeb Serverの実装が出来上がる(はず)。1
学び#
- コンパイル時間短縮の工夫
- アプリケーションを複数のクレートに分割することで変更があったクレートだけ再コンパイルすればよくなるためコンパイル時間が短縮できる。
- たとえば本書ではレイヤードアーキテクチャのレイヤーごとにクレートを分割していた。
- Web開発で有用なクレート
derive_new:new関数を実装してくれる。デフォルト値の設定などもできる模様。serde: 言わずと知れたシリアライズ/デシリアライズのためのクレート。sqlx: SQLを型安全に扱うことができる。
- エラーハンドリングの方法
- クライアントに返すエラーはthiserrorで定義し、それ以外のエラーはanyhowを使って表現する。
- RustでどうDIを実装すべきか
- ジェネリクスを用いてDIするパターン(静的ディスパッチ)
- 動的ディスパッチと比較すると記述量が増える
- 動的ディスパッチと比較するとパフォーマンスが高い
- トレイトオブジェクト(
dyn <トレイト名>)を利用するパターン(動的ディスパッチ)- 静的ディスパッチと比較すると記述量が減る
- 静的ディスパッチと比較するとパフォーマンス面で不利
- 本書ではWeb開発において動的ディスパッチのコストが問題になることは少ないことを理由にこちらを採用していた。
- ジェネリクスを用いてDIするパターン(静的ディスパッチ)
実際にRustでWeb Serverを書いてみた感想(主にGoとの比較)#
※なお、筆者のRustスキルは趣味レベルなのでRustを深く理解したうえでの感想ではありません。間違っている点や他の重要な観点などがある可能性があります。また、主には個人的な考えのスナップショットを自分用に残す目的で書いているので説明を端折っている箇所がある可能性があります。
ポジ#
- enumやパターンマッチ等を使って仕様をスマートに書ける。
- null安全なので他の言語のnull相当の概念を型安全に扱うことができる。
- if式を始めとした関数型ライクな言語機能のおかげで開発体験が良い。
- 変数がデフォルトでimmutableなので、mutableな変数を目立たせやすいし、自然とimmutableなコードが書きやすい。
- あらゆるシンボルがデフォルトでprivateなのでカプセル化するようなコードが若干書きやすい。(意識的に
pubを付けなければprivateになってくれる点で強い理由がなければprivateになりやすくなる、と考えている)
ネガ#
- コンパイル時間が長い。
- コンパイル時間短縮のためにcrate分割が推奨されているが、依存関係の増減があるたびに
Cargo.tomlを手書きで編集しないといけないのが若干面倒。- もしかしたらコードアクション等、エディタの支援が得られるのかもしれないがそこまで調べられていない。
- Goに慣れているとRustのマクロやアトリビュートが若干黒魔術的に感じる。これらに慣れるまでは認知負荷が高そう。例えば以下。(『RustによるWebアプリケーション開発 設計からリリース・運用まで』より引用)
#[derive(Error, Debug)] pub enum AppError { #[error("{0}")] UnprocessableEntity(String), #[error("{0}")] EntityNotFound(String), #[error("{0}")] ValidationError(#[from] garde::Report), #[error("トランザクションを実行できませんでした。")] TransactionError(#[source] sqlx::Error), #[error("データベース処理実行中にエラーが発生しました。")] SpecificOperationError(#[source] sqlx::Error), #[error("No rows affected: {0}")] NoRowsAffectedError(String), #[error("{0}")] KeyValueStoreError(#[from] redis::RedisError), #[error("{0}")] BcryptError(#[from] bcrypt::BcryptError), #[error("{0}")] ConvertToUuidError(#[from] uuid::Error), #[error("ログインに失敗しました")] UnauthenticatedError, #[error("認可情報が誤っています")] UnauthorizedError, #[error("許可されていない操作です")] ForbiddenOperation, #[error("{0}")] ConversionEntityError(String), } - DIが若干複雑
- Goならinterface使うだけなので考えることが少なくて済む。
- 非同期処理
- これもRustの非同期処理の仕組みへの理解2が大変浅いのであまり具体的に書けないが、自分の現状のRustスキルだとGoに比べて意図通りに動く非同期処理を書くのに数倍時間がかかる。
まとめ#
本書を読むまではRustがWebサーバーサイド開発の銀の弾丸、とまでは言わないもののかなりいい選択肢の1つのように思えていた。主に型の表現力のおかげでドメインがうまく表現できたり、所有権などの言語機能や発達したリンターによって内部品質を高く保つことができ、その結果開発生産性が高まると考えていた。
しかし実際に本書でRustでサーバーを書いてみると上記のメリットはありつつも意外とRustでの実装でも手間や複雑なポイントがあり、Goのシンプルさや手軽さの良さを改めて認識した。
結果はともあれ実際にRustでサーバーを書くのがどんな感じなのかを知ることが本書を読んだ目的だったので解像度を高めることができてよかった。3