Web アプリケーションフレームワーク

Introduction

Web アプリケーションフレームワークについて調べたことをまとめる。

Wikipedia の記事 によれば:

Web フレームワーク (WF) または Web アプリケーションフレームワーク (WAF) とは、Web サービス・Web リソース・Web API を含む Web アプリケーションの開発をサポートするように設計されたソフトウェアフレームワークである。(中略)例として、多くの Web フレームワークは、データベースアクセス・テンプレートフレームワーク・セッション管理のライブラリを提供しており、コードの再利用をしやすくしている。動的な Web サイトの開発を主なターゲットとしているが、静的な Web サイトにも適用できる。

History

1989年、イギリスのコンピュータサイエンティスト Tim Berners-Lee は、欧州原子核研究機構 (CERN) で Robert Cailliau とともに World Wide Web – WWWW を作り上げた(参照:A short history of the Web)。この時代の Web はまだ、静的なページをハイパーリンクでつなげ合った集合でしかなく、動的なページはなかった。

1993年、米国立スーパーコンピュータ応用研究所 (NCSA) が Common Gateway Interface (CGI) を定義した。CGI という技術は、Web サーバが HTTP リクエストごとに CGI 用プログラムを起動・実行して、その出力によって動的に Web ページを生成する。しかし、HTTP リクエストごとに CGI 用プログラムが起動されるため、負荷が大きい。

1996年、CGI の問題に対応するために mod_perl (1996)Java Servlet (1996-1997)Active Server Pages (1996) が出て来た。これらは HTTP リクエストを処理するプログラムがリクエストごとに起動されるのでなく、デーモン的にずっと起動している1つのプログラムが何度も HTTP リクエストを処理してくれる。また、PHP (1995)、Active Server Pages、JavaServer Pages (1999) にあるような HTML テンプレート機能も導入された。HTML テンプレート機能は、HTML ページのうち動的に変わりうる箇所だけスクリプト(プログラム)を埋め込んで書ける機能で、動的なページの生成には便利だった。

最初の Web アプリケーションフレームワークが何なのかというのは難しいが、ともあれ1995年前後に、Web アプリケーションフレームワークの最初期段階が同時多発的に生まれていった。NeXT の WebObjects (1996) が、MVC パターンを GUI でなく Web アプリケーションに適用して世に広めたとも言われているが、当時の情報が少なくて分からない。

1999年、Java 2 Platform, Enterprise Edition (J2EE) がリリースされた。これは Java で Web アプリケーションなどのエンタープライズアプリケーションを開発するための統合的な Java API である。ここには前述の Java Servlet や JavaServer Pages のほか、ビジネスロジックを定義するための Enterprise JavaBeansも含んでいる。ほかにも RDB とのインタフェースとなる JDBC、メールを扱う javax.mail もある。ただ、高機能な RDB 処理ライブラリである Java Persistence API (JPA) は Java EE 5 (2006) になるまで待たなければならない。

後述する書籍 “Patterns of Enterprise Application Architecture” (2002) のコードサンプルが当時の空気感を理解しやすい:

web.xml:

<servlet>
  <servlet-name>artist</servlet-name>
  <servlet-class>actionController.ArtistController</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>artist</servlet-name>
  <url-pattern>/artist</url-pattern>
</servlet-mapping>

Java コード:

class ActionServlet extends HttpServlet {
  protected void forward(String target, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
    RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(target);
    dispatcher.forward(request, response);
  }
}

class ArtistController extends ActionServlet {
  public void doGet(HttpServleRequest request, HttpServletResponse response) throws IOException, ServletException {
    Artist artist = Artist.findNamed(request.getParameter("name"));
    if (artist == null)
      forward("/MissingArtistError.jsp", request, response);
    else {
      request.setAttribute("helper", new ArtistHelper(artist));
      forward("/artist.jsp", request, response);
    }
  }
}

2002年、ASP.NET がリリースされた。前述の Active Server Pages (ASP) 技術を .NET Framework 上で置き換えたものだが、ASP とは別物で、代わりに Web Forms という概念が導入されている。J2EE と ASP.NET はエンタープライズにおける代表的な Web アプリケーションフレームワークとなった。

同じく2002年、Martin Fowler らは技術書 “Patterns of Enterprise Application Architecture” (PofEAA) を出版した。タイトル通り、エンタープライズアプリケーションのアーキテクチャのパターンを記述しているが、これはそのまま Web アプリケーションのアーキテクチャのパターンだった。MVC、DTO、アクティブレコード、リポジトリ、ドメインモデル、サービスレイヤーなど、現代でも使われている様々なパターンがここでまとまって解説されている。

2004年、David Heinemeier Hansson は Ruby on Rails を発表した。これは PofEAA で紹介されたパターンを使いやすく取り込んでおり、世界で最も有名な Web アプリケーションフレームワークになった。

Dan Kegel が1999年に論じた C10K problem はのちに有名になっていった。10000以上のクライアントが同時にアクセスしても耐えられるサーバはどうすれば作られるかという話である。Apache のように、クライアントごとに別々の OS スレッドで処理させるアーキテクチャでは、クライアントの同時アクセスが増えればスレッドも増えて、限界が生じる。特に、Ruby on Rails 以降、Web アプリケーション開発用言語として広く採用されるようになった Python や Ruby は、グローバルインタープリタロック (GIL) を持っていたり並行プログラミングが得意でなかったりして、スケールアウトさせることは難しかった。そんな中でメジャーになっていったのが、並行プログラミングまたは非同期プログラミングを得意とする Erlang、Node.js、Go である。

Erlang はスウェーデンの通信機器メーカーであるエリクソンで、Joe Armstrong らが開発したプログラミング言語だ。1986年に作られたらしいが、1998年にオープンソースとして公開され(History of Erlang)、2007年には Jon Armstrong が “Programming Erlang: Software for a Concurrent World” を出版した。Prolog の影響がある文法による関数型プログラミング言語で癖はあるが、それを超えてアクターモデルをもとにしたパワフルな並行プログラミングを可能にする。Erlang のプロセスは軽量スレッドであり、非同期で任意のオブジェクトをメッセージとして送信したり受信したりできる。OS ネイティブスレッドと違って、Erlang のプロセスは驚くほど軽量なため、C10k problem への解決策になった。

2009年に Ryan Dahl が公開した Node.js は、Google Chrome の高速な JavaScript エンジンである V8 をもとにした JavaScript 処理系で、libev/libuv ライブラリを使った非同期 I/O を実現している。Node.js 自体はほぼシングルスレッドで動くが、非同期プログラミングによって I/O 箇所はマルチスレッドのように動く(マルチスレッドのコストなしで)。興味深いことに、素直にスレッドプールでリクエストごとに別スレッドが処理を担当するような実装よりも、Node.js のようなシングルスレッドの非同期 I/O 処理の方が C10k problem への解決策になった。

2009年に公開された Go は、CSP をもとにした並行プログラミングを可能にする。軽量スレッドであるゴルーチンと、ゴルーチン間で値を送受信して通信するためのチャネルがある。Erlang のアクターモデルと比べると、プロセスにメールボックス(チャネル)が紐付いているのでなく、ゴルーチンとチャネルを明確に分けている点が違うが、おおよそ同じようなもので、C10k problem への解決策になった。

Web Application Architecture

PofEAA の Layered Architecture

2002年、Martin Fowler は前述の技術書 PofEAA で Web アプリケーションのアーキテクチャについて論じた。第1章の「レイヤ化 (Layering)」では、Layered Architecture をこうまとめている:

  • Presentation Layer: ユーザからの入力を扱って Domain Layer に実処理を任せたり、HTML ページなどの表示を行ったりする。
  • Domain Layer: ビジネスロジックを行う。Presentation Layer からユーザの入力を受け取って、バリデーションしたり、Data Source Layer とつなげて永続的なデータの操作をしたりして、Presentation Layer に表示用のデータを返す。
  • Data Source Layer: データベースや他のシステムと通信して、永続的なデータの操作などを行う。

この3層構造は Three-Tier Architecture として知られるが、レイヤードアーキテクチャ自体はこの3層に限定していない。

Alistair Cockburn の Hexagonal Architecture

PofEAA では Alistair Cockburn の Hexagonal Architecture も紹介している。Hexagonal Architecture というコンセプトが生まれた年は不明だが、Kevin Rutherford によれば1998年頃にはあったというらしい。ただ、その後も徐々にコンセプトを進化させていったんだとは思う。Hexagonal Architecture は対称的なアーキテクチャである:

  • Adapter Layer: 外部からの入力を受け取って、Application Layer が扱える形式に変換して、Application Layer に引き渡す。また、Application Layer から出力を受け取って、外部システムが受け取れる形式に変換して、外部システムに引き渡す。デザインパターンの Adapter パターン にあたる。
  • Application Layer: Adapter Layer の内側にあり、ビジネスロジックを担う。

DDD の Layered Architecture

2003年、Eric Evans は技術書 Domain-Driven Design (DDD) を出版した。ドメイン駆動設計という PofEAA の次に重要なコンセプトが論じられており、ここでの Layerd Architecture は PofEAA と少し異なる:

  • User Interface Layer: PofEAA の Presentation Layer と同じ。
  • Application Layer: ドメインロジックを含まない、アプリケーションサービスを担う。アプリケーションサービスとドメインロジックの区別は少し難しいので、後に扱う。
  • Domain Layer: ドメインロジックを担う。PofEAA の Domain Layer は、DDD の Application Layer と Domain Layer を合わせたもの。
  • Infrastructure: PofEAA の Data Source Layer と同じ。

PofEAA と DDD で共通しているのは、User Interface -> Application/Domain -> Infrastructure という層の依存関係である。ここで Application/Domain が Infrastructure に依存しているのは微妙だという批判がある。これに対して対称的な Hexagonal Architecture は、内側に Application Layer があって、外側に UI も Infrastructure もまとめた Adapter Layer があるため、 Application Layer は何にも依存しない。確かにこの方がクリーンだが、Application/Domain Layer はどうしても Data Source への操作を行わなければならないから、「依存の逆転」が必要になる。

Jeffrey Palermo の Onion Architecture

2008年、DDD のような Layered Architecture に対して「依存の逆転」をすることで Hexagonal Architecture スタイルにしたのが、Jeffrey Palermo の Onion Architecture だった:

  • Domain Model Layer: ドメインモデルを担う。円状のレイヤーで、一番内側にある。
  • Domain Service Layer: ドメインサービスを担う。Domain Model Layer の1つ外側にある。
  • Application Service Layer: アプリケーションサービスを担う。Domain Service Layer の1つ外側にある。
  • UI/Infrastructure/Test Layer: DDD の UI Layer と Infrastructure Layer と同じ。一番外側にある。

Robert C. Martin の Clean Architecture

2012年、こうしたアーキテクチャをさらにまとめ直したのが、Robert C. Martin の Clean Architecture だった:

  • Entity Layer: DDD の Domain Layer と同じ。円状のレイヤーで、一番内側にある。
  • Use Case Layer: DDD の Application Layer と同じ。ただ DDD では Application Layer は薄くあるべきと言うが、Clean Architecture では必ずしもそうでない、ドメインサービス的なロジックがなかばここに吸収されることもある。Entity Layer の1つ外側にある。
  • Interface Adapter Layer: Hexagonal Architecture の Adapter Layer と同じ。外部インタフェース用から渡ってきたデータを、Use Case Layer / Entity Layer 用の形式に変換したり、その逆変換をして外部インタフェースに渡したりする。Use Case Layer の1つ外側にある。
  • Infrastracture Layer: Web フレームワークや DB そのもの。一番外側にある。

Microservice Architecture

2014年、Martin Fowler と James Lewis は Microservices アーキテクチャを提唱した。このアーキテクチャでは、1つのアプリケーションをサービス(プロセス)の集合体として実装して、サービス間は Web API や RPC を通して通信する。Amazon や Netflix でこのアーキテクチャは採用されていた。

  • Good: サービスごとに個別にデプロイ可能になる。
  • Good: サービスごとに cross-functional なチームを作る。それぞれのサービス開発チームは、自由にプログラミング言語やツールを選べる。
  • Good: サービスが DDD の Bounded Context にあたり、自然に抽象化される。
  • Bad: コンポーネント(サービス)を正しく分けて構成することが、とても重要になる。
  • Bad: サービス間のインタフェースを変更するようなリファクタリングをするのは、とても大変になる。

上記記事でも言われ、それ以後もよく言われていることが「モノリスから始めて、モジュール化を保っておく。モノリスのアーキテクチャで問題が出て来たタイミングで、マイクロサービスに移行する」。

Anti-Architecture

理想的なアーキテクチャ設計の考え方は成熟していったが、現実のソフトウェア開発を助けてくれたわけでは必ずしもなかった。

  • Ruby on Rails のように Web アプリケーションフレームワークが自動でアーキテクチャを与えてくれていたときはよかった。Web アプリケーション開発者自身がアーキテクチャを構成するとなると、そこに労力と設計センスが求められる。
  • 理想的なアーキテクチャは、理想的な設計をすれば成功する。しかしバグのないソフトウェアはないように、理想的な設計をしたソフトウェアもない(あるいは稀である)。理想的な設計を妥協するのであれば、妥協したアーキテクチャを選んでもよい。
  • ごくシンプルな Web アプリケーションでは Microservice Architecture や Clean Architecture はオーバースペックである。
  • Microservice Architecture を選ぶとき、サービスごとに開発チームを作るほど開発メンバーは潤沢にいるのか? 実際には同じメンバーが複数サービス(最悪の場合、全サービス)を担当するのでないか? あるいは、各サービス開発チームはすべて、サービス間の通信を正しく設計できる能力があるか?
  • Clean Architecture を選ぶとき、開発メンバーは全員、Clean Architecture を理解して従う意思があるか? コーダーとレビュアーの二人ともアーキテクチャ理解が足りない瞬間があればもう、Clean Architecture に反するコードが紛れ込む。

一流の大企業でなければ、開発人員と開発スキルは常に足りていない。彼らのためのアーキテクチャは誰も考えていない。開発者もユーザーのようなものだ。

Modern Web Application Frameworks

Ruby on Rails

  • Good: 今でもメジャーな Ruby の Web アプリケーションフレームワーク。
  • Good: 洗練された使いやすい API を持つ。
  • Good: 長い歴史により、高機能と高安定性を備えている。
  • Good: プロジェクトのディレクトリ構成があらかじめ作られたり、様々な Conventions が定義されていたりするため、それに従って書くだけで、おおよそのレイヤードアーキテクチャとモジュラリティが保たれる。
  • Bad: ActiveRecord パターンが採用されているため、Domain Model と Repository パターンが1つのクラスに合わさっているような状態。その結果、Fat Model になりやすいという問題がある。もっと適当に書くと Fat Controller になりやすい。サービス層やフォームオブジェクト層は意識して作る必要がある。
  • Bad: Ruby なので、C10k 問題には厳しい。Ruby で強力な非同期 Web サーバが実装されればよいが、今はまだ puma 全盛のようだ。
  • Bad: Ruby なので、メモリ消費や処理速度は期待できない。
  • Bad: Ruby on Rails によって一躍メジャーな言語になった Ruby だが、近年 Ruby は廃れ始めている。

Hanami は、Ruby on Rails を DDD/Hexagonal/Clean Architecture の考え方に寄せて洗練させようとした Ruby の Web アプリケーションフレームワークである。サービス層として Interactor を導入したり、コントローラのアクションごとに1つのクラスにしたり、Repository パターンを導入したり。わたしは Hanami は結構好きなのだが、Ruby on Rails のコントローラや ActiveRecord がひどい欠点というわけでもないので、であればメジャーで高機能な Rails を使えばいいかなと思ってしまっている。

FastAPI

  • Good: Python の Web アプリケーションフレームワークとしては、近年メジャーになっている。
  • Good: 後発のフレームワークだけあって、Python 言語や各種ライブラリの最新機能を活用して、洗練された使いやすい API を持つ。
  • Good: ASGI フレームワーク Starlette を利用して非同期 I/O を実現しているため、Python でありながらハイパフォーマンス。
  • Good: OpenAPI との親和性が高い。
  • Bad: 名前の通り、API サーバを作るためのフレームワークであって、HTML/CSS/JavaScript を操作するための便利な機能は備えていない。Jinja2 による HTML テンプレートを使うくらいなら簡単にできるが、それ以上は難しい。
  • Bad: Python なので、メモリ消費やロジック処理速度は期待できない。非同期 I/O によって HTTP リクエスト処理は高速だが、CPU-bound な処理はマルチコアを活用できない。
  • Bad: DB 操作は、任意の Python ライブラリに任せており、公式ドキュメントでは SQLAlchemy の例 が書かれている。FastAPI との親和性は特にないので(親和性が悪いわけでもない)、注意を要する。
  • Bad: プロジェクトのディレクトリ構成もモジュール構造も名前も自由のため、適当に書くとレイヤーもモジュラリティもないコードになる。

Echo

  • Good: Go によるマイクロ Web アプリケーションフレームワークで、シンプルかつ一定の機能を備えている。
  • Good: Go なのでハイパフォーマンスで、メモリ消費も少ない。
  • Good: 同じ Go の Web アプリケーションフレームワークとしては Gin があるが、Gin より後発なだけあって、API は少しだけ Gin より綺麗。
  • Bad: フルスタックでない Web アプリケーションフレームワークなので、機能は少ない。必要に応じてほかの Go ライブラリを選定して利用することになる。Echo 自体は悪くないライブラリだが、ほかの Go ライブラリの信頼性は不明なので、そこで罠にはまる可能性が高い。
  • Bad: DB 操作のライブラリは、そもそも Go であまりよいものがない。ORM の GORM は強力だが、まだ少しバギーなのと、あまり型安全でない。最近では Ent が型安全でよいが、まだ機能が足りておらず、やはりバギー。(バギーである証拠は、単純にわたしが普通に単純なアプリケーションを書いていて、どちらもバグに遭遇したこと)
  • Bad: パッケージ構成が自由なので、レイヤードアーキテクチャにするためにはかなり意識した設計を必要とする。
  • Bad: Go の Web アプリケーションすべてが遭遇する問題だが、ユニットテストを書くのが大変。

ASP.NET Core

  • Good: Microsoft による .NET 上で動く Web アプリケーションフレームワークで、ASP の後継である ASP.NET の後継。ASP、ASP.NET は基本的に Windows 上でしか動かなかったが(Mono を使うなどしなければ)、ASP.NET Core は クロスプラットフォームの .NET Core に合わせて作られたため、Linux や macOS 上でも動く。
  • Good: MVCRazor テンプレート、バリデーション、Scaffolding、Blazor による JavaScript なしでのインタラクティブ UI、SignalR による WebSocket を使ったリアルタイムアプリ、Entity Framework による DB 操作、Identity による認証認可など、Ruby on Rails と同等以上の、世界トップクラスの高機能を提供している。
  • Good: .NET で動くため、ハイパフォーマンス。
  • Not Bad: .NET で動くライブラリのため、C# だけでなくほかの言語でも利用可能。ただ現実には、dotnet new でアプリケーションを作るとき多くのテンプレートや Scaffloding は C# しか選べない。C# 以外の言語で ASP.NET Core をやろうとしても恩恵が少ない。せっかく ASP.NET Core を採用するなら C# がよい。
  • Bad: .NET Core / ASP.NET Core はクロスプラットフォームだが、以前の ASP や .NET / ASP.NET は Windows 専用だった長い歴史もあり、非 Windows 環境上の .NET 利用についてはメジャーでない。
  • Bad: .NET と ASP.NET Core 特有の設計思想やツールチェイン上に作られているため、.NET 以外の好みが分かれる。
  • Bad: .NET と ASP.NET Core の学習曲線はかなり険しい。しかも UNIX 的な世界観に慣れていても役に立たない。

参照: https://github.com/gothinkster/aspnetcore-realworld-example-app, https://github.com/ardalis/cleanarchitecture

Nest

  • Good: Node.js (TypeScript/JavaScript) の Web アプリケーションフレームワークで、実績ある Express または Fastify 上で動く。
  • Good: Angular の影響を受けたアプリケーションアーキテクチャでバックエンドアプリケーションが作られるように設計されており、自然とモジュラリティの高いコードになる。
  • Good: Node.js の強力な非同期 I/O によって、ハイパフォーマンスを実現している。V8 エンジンによってロジック処理速度も高速。
  • Bad: フルスタックでない Web アプリケーションフレームワークなので、フロントエンドや DB 操作は関わらない。現実には、好きなフロントエンドと DB 操作ライブラリ(今なら Prisma がいいか)を選んで学習・利用する必要がある。
  • Bad: Angular のアプリケーションアーキテクチャ思想に従っているが、これは独特なので好みが分かれる。前述したようなレイヤードアーキテクチャとはあまり合致しない。
  • Bad: 非同期 I/O なので高速だが、基本的にシングルスレッドなのでマルチコアを活用できず、パフォーマンスに限界がある。Requests/sec を測るシンプルなベンチマークでは高速だが、これは CPU-bound でなく I/O-bound であれば非同期 I/O が十分に効果を発揮するからであって、CPU-bound な処理を含むアプリケーションではマルチコアを活用したい。

Nest の Angular-inspired アーキテクチャについては説明を要する。

  • Controller cats.controller.ts: リクエストを受け付けて、レスポンスを返すオブジェクト。@Controller('PATH') デコレータを付けてクラスを宣言する。
    • MVC の Controller そのもの。
  • Provider cats.service.ts: 依存注入 (DI) 可能なオブジェクト。@Injectable() デコレータを付けてクラスを宣言する。
    • @Injectable() class CatsService を宣言しておけば、別クラスのコンストラクタ constructor(private catsService: CatsService) には自動で CatsService のインスタンスが渡されて依存注入される。Providers | NestJS 参照。
  • Module cats.module.ts: モジュールとして機能をまとめあげる。@Module({ controllers: [...], providers: [...], imports: [...], exports: [...] }) デコレータを付けてクラスを宣言する。
    • モジュール Cats 内のプロバイダ CatsService は、(1) Cats から CatsService をエクスポートして、(2) 利用したい別モジュールから Cats をインポートしなければ、利用できない。
  • Middleware logger.middleware.ts: Express のミドルウェアと同じもの。@Injectable() デコレータを付けて NestMiddleware インタフェースを実装してクラスを宣言する。
  • Exception Filter http-exception.filter.ts: 例外フィルタ。ExceptionFilter インタフェースを実装してクラスを宣言する。
  • Pipe validation.pipe.ts: パイプ。@Injectable() デコレータを付けて PipeTransform インタフェースを実装してクラスを宣言する。
    • パイプは通常、コントローラの引数に対して @Body(), @Query(), @Param() のようなデコレータを付けることで、入力データの変換処理やバリデーション処理を行う。
  • Guard auth.guard.ts: ガード。@Injectable() デコレータを付けて CanActivate インタフェースを実装してクラスを宣言する。
    • ガードは通常、コントローラに対して @UseGuards(AuthGuard) のようにデコレータを付けることで、認証・認可のようなチェックを行う。
  • Interceptor logging.interceptor.ts: アスペクト指向プログラミングのインターセプタ。@Injectable() デコレータを付けて NestInterceptor インタフェースを実装してクラスを宣言する。

Blitz

  • Good: Node.js (TypeScript/JavaScript) のフルスタック Web アプリケーションフレームワークで、Next.js や Prisma のような最新の強力なライブラリを採用している。
  • Good: Ruby on Rails の影響を受けており、(といっても Rails っぽいディレクトリ構成やアーキテクチャではないけれど)その思想に従って、使いやすい API やアーキテクチャを目指している。
  • Good: Node.js の強力な非同期 I/O によって、ハイパフォーマンスを実現している、と思われる。V8 エンジンによってロジック処理速度も高速。
  • Good: フロントエンドは Next.js なので、React によるシンプルで強力な UI コンポーネント操作ができるし、Next.js による SSG/SSR もできる。フロントエンドがどんどん進化していって、jQuery で DOM を触る牧歌的な時代は大昔に終わり、フロントエンドとバックエンドとのコードの親和性を維持することはできなくなった。しかし Blitz であれば、フロントエンドとバックエンドを1つの言語・フレームワークで記述してまとめ上げることができる。
  • Bad: まだ新しいフレームワークで、安定性は不明。
  • Bad: React, Next.js, Prisma(別の DB ライブラリを使えないわけではない)、フォームライブラリなどの知識を別に必要とするため、学習は意外と大変。また、それらの「巨人の肩」群を床に配置して Blitz という糸で結び付けただけといえばそうなので、全体の親和性は高くない。
  • Bad: 非同期 I/O は高速だが、シングルスレッドなのでマルチコアを活用できず、パフォーマンスに限界がある。
  • Bad: バックエンド部分については機能が足りていない。詳しくは後述するが、Query/Mutation 関数で自動で作られる RPC API は公開を想定しているものでない。

Blitz には、サーバ上のデータを取得する Query とサーバ上のデータを更新する Mutation というコンセプトがある。Query と Mutation はサーバ上で動く非同期 JavaScript 関数で、クライアントサイドでの Query/Mutation の呼び出しは自動で RPC(サーバ API)に置き換わるし、useQuery, useMutation ごしに呼ぶことになっている。RPC 仕様によれば、シンプルに、POST /api/rpc/{関数名} エンドポイントに対して JSON でパラメータを送ると JSON レスポンスが返ってくる形式になっている。Manifesto の “2 API Not Required” にあるように、Blitz のユースケースは API 公開をほとんど想定していないため、REST API でも GraphAPI でもない独自の HTTP RPC API でもよいということなんだろう。

TypeScript/JavaScript で DB 操作を利用する場合、今では PrismaTypeORM がメジャーだと思う。TypeORM は Rails や Django のような ActiveRecord パターンであり、Fat Model 問題につながりやすく、Prisma の方がモダンであるとされるが、好みの差。Prisma を少しだけ触っただけだが、まだ新しいライブラリで、まだ安定性というか細かい部分の挙動制御が足りていないように思われた。が、具体的に何だったか忘れたから、間違いかもしれない。

Modern Requirements

モダンな Web アプリケーションフレームワークの要件を考える。

Routing

Echo は、URL パスとコントローラ関数とのマッピングを記述することで、ルーティングを定義する:

func postsList(c echo.Context) error { ... }
func postsGet(c echo.Context) error { ... }
func postsCreate(c echo.Context) error { ... }

func main() {
  e := echo.New()
  e.GET("/posts", postsList)
  e.GET("/posts/:id", postsGet)
  e.POST("/posts", postsCreate)
}

Ruby on Railsconfig/routes.rb という専用のファイルがあって、そこで URL パスとコントローラ・アクションとのマッピングを定義する。コントローラ・アクションは文字列で表現され、admin/articles#indexAdmin::ArticlesController#index に対応する。

class PostsController
  def list; end
  def show; end
  def create; end
end

Rails.application.routes.draw do
  get 'posts', to: 'posts#index'
  get 'posts/:id', to: 'posts#show'
  post 'posts', to: 'posts#create'
end

FastAPI はコントローラ関数に対して URL パス情報を付与したデコレータを設定する。

app = FastAPI()

@app.get("/posts")
async def postsList(): ...

@app.get("/posts/{id}")
async def postsList(id: str): ...

@app.post("/posts")
async def postsList(): ...

ルータの効率的なアルゴリズムは、suffix tree またはその亜種が使われている。

Controllers

コントローラ層は、エンドポイントごとにコントローラ(アクション)関数を定義する。コントローラ関数は HTTP リクエストを受け付け、パラメータなどの情報を内側の層に渡して実処理を行わせて、その結果を使って HTTP レスポンスを返す。

コントローラ層は HTTP リクエストの様々な情報を取得できるが、その中でも Cookie の取得や IP アドレスの取得については使いやすい関数をフレームワークが用意する。

コントローラ層では、入力パラメータの変換処理を行う。たとえば JSON から構造体に変換するなどのバインディング処理がこれにあたる。一方で、バリデーション処理はコントローラでなくその内側の層でやるべきはず。

出力のテンプレート適用処理(ビュー層への処理の依頼)や Cookie の設定処理は、コントローラ層で行うべきはず。

Usecases, Services, Interactors

この層では、実際のリクエストに対する実処理を定義する。コントローラ層から、この層の実処理を呼ぶ。

DB Migration

Ruby on Rails の DB マイグレーションは、(1) DB スキーマを Ruby の DSL コードで記述して、モデル層やリポジトリ層と別に管理する。(2) DB スキーマのコードはバージョンごとに新規ファイルで管理される。まずは bin/rails generate migration CreateProducts コマンドを実行すると db/migrate/YYYYMMDDHHMMSS_create_products.rb というファイルが生成されるので、それを書き換えて追加・更新したい DB スキーマを Ruby で記述する:

class CreateProducts < ActiveRecord::Migration[7.0]
  def change
    create_table :products do |t|
      t.string :name, limit: 255, null: false
      t.text :description, default: '', null: false
      t.timestamps, null: false
    end
  end
end

これを本番リリース後、また DB スキーマを更新することになれば、既存のマイグレーションファイルはそのままで、新しいマイグレーションファイルを作る。Rails は暗黙に schema_migrations という DB テーブルを作っており、そこでどのマイグレーションファイルを実行ずみか記録している。bin/rails db:migrate コマンドで、まだ実行されていないマイグレーションファイルを実行して、DB マイグレーションを行う。bin/rails db:rollback コマンドはその逆で、ロールバックを行う。

PrismaEnt は、あらかじめスキーマ定義を DSL で記述しておいて、それをもとにライブラリが型安全な DB 操作インタフェースを提供してくれたり、DB マイグレーションを行ってくれたりする。Prisma は DB マイグレーション自体は行わず、DB マイグレーション用の SQL ファイルを生成する。

Repositories / ActiveRecords

DB の操作は、Repository パターンまたは ActiveRecord パターンを通して行う。

Models

この層では、ドメインモデルを定義する。

Domain Service

この層では、ドメインロジックレベルのサービスオブジェクトを定義する。

View

ビュー機能、テンプレートを実装する。

  • HTML や JSON データのレスポンス。
  • HTML テンプレートのレスポンス。
  • 静的ファイルのレスポンス。

Validation / Form Object / Request Object

コントローラ層で受け取ったリクエストのパラメータは、バリデーションまたはフォームオブジェクト・リクエストオブジェクトといわれるクラスを通して、あらかじめバリデーションを行う。

Response Object

ユースケース層で実処理をしたあと、レスポンスオブジェクトといわれるクラスを通してレスポンス情報を返す。通常コントローラ層が、このレスポンスオブジェクトを適切な形式(JSON や HTML)に変換してレスポンスとして返す。

Session Management

セッション管理の実装についてはあまり語られることがない。

  • セッション情報を保存するデータストア(セッションストア)は、要件によって適切なものが異なるため、自由に選べることが望ましい。
  • リクエスト Cookie に含まれるセッションIDをもとにセッションストアからセッション情報を取得する。普通、同じリクエスト内では、最初にセッションストアから取得したらあとはメモリ上にキャッシュしておく。
  • 新規セッションの場合、ランダムな新規セッションIDを発行する。
  • セッション情報を更新したら、すぐに、またはそのリクエスト処理の最後でまとめて、セッション情報をセッションストア上でも更新する。
  • セッション情報を永続データストアに保存するときは、なんらかのシリアライズの方法が必要となる。シリアライズの形式は、将来の実行言語処理系の変更を考えれば JSON や CBOR のようなポータブルな形式が望ましい。というより、シリアライズの形式を自由に選べるようなセッション管理ライブラリであることが望ましい。
  • Cookie にセッションIDをセットしてレスポンスを返す。
  • Cookie と、セッションストアに保存されたセッション情報については、セッションの有効期限を適切に設定する。
  • セッション固定攻撃への対策としてログイン前後ではセッションIDを変更する必要があるため、セッションIDを変更する機能が必要となる。
  • ログアウト時には、セッションを破棄する。

Remote IP Addresses

どんな Web アプリケーションフレームワークや HTP サーバライブラリでも、クライアントの IP アドレスを取得する機能はある。ただ、その取得の仕方は単純でない。

  • 素直に IP アドレスを取得すると?
    • 直に Web アプリケーションがリクエストを受け付けている場合、問題ない。しかし、Web アプリケーションとクライアントの間にリバースプロキシなどを挟んでいる場合、素直にクライアント IP アドレスを取得するとそれはリバプロの IP アドレスになる。
  • リバプロでなくクライアント IP アドレスを取得するために X-Forwarded-For ヘッダなどの値を使うと?
    • ヘッダの中には、クライアント、クライアント側のプロキシ、サーバ側のリバプロといった複数の IP アドレスが格納されている。このとき、IP アドレスの配列の中で、どの値を使うべきか注意しないといけない。
  • IP アドレスの取得を間違えると?
    • Web アプリケーションが自前でクライアント IP アドレスを見て ACL をかけているような場合、クライアント IP アドレスを正しく取得できていないと脆弱性になりうる。

メジャーでないライブラリやミニマル志向のライブラリは、こういうところがケアされていない可能性が高い。メジャーなライブラリでも、過去に一度問題を踏んでから慌てて直している形跡がよく見られる。そのため、ライブラリを過信せず、利用者側も目を凝らしておくべき問題だと思う(そもそも、利用環境によって話が変わってくるので)。

IP アドレスの取得の仕方については、IP Address | Echo の記事がよいか。

  • Ruby on Rails
    • ActionDispatch::RemoteIp ミドルウェアで実装されている。コメントにも書かれている。2012年に IP アドレスの取得の仕方を間違えてしまって、2013年に直している
  • Express
  • Echo
    • 比較的新しいフレームワークとはいえ、この問題に対応したのは、やっと2020年だった。
  • Gin
    • 比較的新しいフレームワークとはいえ、この問題に対応したのは、後発の Echo よりも遅くやっと2021年だった。
  • FastAPI
    • FastAPI 自体は扱っておらず、uvicorn などがこの問題を取り扱う。Trusted Hosts に対応したのは2021年。ただ、それ以前の2018年から、Trusted Hosts 関係なく X-Forwarded-For ヘッダの一番右の IP アドレスを使っていたから、問題の発生するケースはあったものの、一番左の IP アドレスを使うよりはまだましだったかと思う。

Mail

一部の Web アプリケーションではメールの送信機能が必要になる。これは言語標準ライブラリの範囲でも通常なんとかなるが、Ruby on Rails の Action Mailer などではより便利な機能が付与されている。

HTTP Client

バックエンドの Web アプリケーション上で、連携している外部サービスの HTTP API をリクエストする必要があることがある。HTTP リクエストを送るだけなら言語標準ライブラリの範囲で問題ないが、リトライ機構は用意しなくてはならない。

CSRF Token

CSRF 対策に、CSRF トークンが必要となる。CSRF トークンはセッション管理が関連してくるため、セッション管理ライブラリと同レベルかそれ未満のライブラリしか存在しない。Ruby on Rails は CSRF トークンを自動で付与して自動でチェックしてくれるため、ほかのフレームワークより使いやすい。

OpenAPI

Web API は OpenAPI (Swagger) でドキュメント化することが望ましい。OpenAPI ライブラリは多くの言語にあるが、普通にエンドポイントを実装するだけで、そこから自動で OpenAPI ドキュメントを生成してくれる Web アプリケーションフレームワークは少ない(あるいは、あまり使い勝手がよくなくて、結局手動で OpenAPI ドキュメントを記述した方がよい)。例外は最初から OpenAPI との親和性を意識した FastAPI。

Encryption

  • パスワードのハッシュ化。
  • データベースの特定カラムの暗号化。
  • Cookie に直接情報を保存するときの情報の暗号化。

I18n

WebSockets

Created at