私はAccel RecordというTypeScript用ORMライブラリを開発しています。
Accel Recordは他のTypeScript/JavaScript用ORMライブラリとは異なり、非同期APIではなく同期APIを採用することにしました。
ORMのインターフェースを検討する過程で、非同期APIと同期APIについて利点や欠点を比較することとなりました。この記事ではそれらについて整理し、サーバーサイドTypeScriptでも、非同期処理を使わず同期処理で開発できるという選択肢が必要なのではないかという考えについて述べたいと思います。
TypeScript/JavaScriptの非同期処理とは?
サーバーサイドでJavaScriptを実行する場合、Node.jsを利用することが多いです。
Node.jsはシングルスレッドで動作する非同期I/Oモデルを採用しており、アプリケーションの実装を非同期処理で行うことが一般的です。
非同期処理の書き方は歴史的にも変遷してきましたが、現在ではasync/await
を使った書き方が主流かと思います。
// 非同期処理を行う関数は、`async`をつけてPromiseを返す
const fetchUsers = async (): Promise<User[]> => {
// DBからユーザーを取得する処理
// ...
};
// awaitを使って非同期処理の結果を待つ
const users = await fetchUsers();
このように、非同期処理を行う関数はasync
をつけてPromiseを返すように実装します。
呼び出し側では、await
を使って非同期処理の結果を待ちます。
過去のJavaScriptではコールバック関数を使って非同期処理を実現していましたが、近年ではasync/await
を使うことで、非同期処理の記述がより直感的になりました。
開発体験としては大きく改善されましたが「呼び出す関数が非同期処理を行うかどうかを意識する必要がある」ことは現在も変わっていません。
非同期関数をawait
を使わずに呼び出してしまったことで意図しない挙動を引き起こすこともあります。
そのため、呼び出す関数が同期的か非同期的かを考慮し、非同期の場合はawait
を書くかどうかを判断する、という作業が逐一必要になります。
サーバーサイドで使われる非同期処理
非同期処理の呼び出しがほとんど必要ないのであれば、上記のような作業はあまり問題にならないかもしれません。
しかし、Webアプリケーションなどのサーバーサイドの開発では、非同期処理を使うことが多いです。
なぜなら、JavaScriptのライブラリではDBアクセスは基本的に非同期処理として実装されているためです。
Webアプリケーションのサーバーサイド処理では、HTTPリクエストを受け取り、データ取得や書き込み等のDBアクセスを行い、最終的な結果をHTTPレスポンスとして返す、という処理が主な流れです。
より複雑なアプリケーションになるほど、DBアクセスが必要となる箇所も増え、非同期処理を使うことが多くなるケースが多いと思います。
非同期処理を書く箇所が多いと、上記のようにawait
を書くかどうかを意識する必要が増えます。非同期処理を全く使わない場合と比べると、アプリケーション自体の開発ではなくそのような細かい部分をケアするコストが増え、開発効率が下がってしまうことになります。
非同期処理を使う利点
では非同期処理は、どのようなメリットがあり使われているのでしょうか。
最も大きな利点は、システムのパフォーマンス向上です。
Node.jsなどの実行環境では、非同期処理を使うことでI/O待ちの時間を他の処理に活用できます。
その結果、非同期処理を使わない場合よりもシステム全体のパフォーマンスが向上することが期待できます。
例えば、HTTPリクエストを受け取った際にDBアクセスを行う処理がある場合、DBからレスポンスが返ってくるまでの間に他のリクエストを受け付けることができるようになります。
Node.jsではイベントループという概念があり、適切に非同期処理を使うことでイベントループをブロックしないようにすることがパフォーマンス上重要とされています。
非同期処理を使わない、という選択肢
しかし私はORMのインターフェースを検討する過程で、JavaScriptの実行環境だからといって必ずしも非同期処理を使う必要はないのではないかと考えるようになりました。非同期処理前提だと、DBアクセスが発生する可能性があるメソッドでは常にawait
を書く必要があり、そのことがライブラリの抽象化を難しくしていると気づいたからです。DBアクセスが同期処理であれば、理想のインターフェースを実現してより使い勝手の良いライブラリになると感じました。1
同期処理中心でアプリケーションを実装すれば、非同期処理を使う場合と比べてシステムのパフォーマンスは一部低下するかもしれません。しかし「呼び出す関数が非同期処理かどうかを見てawait
をつけるべきか判断する」という手間が減るというメリットもあります。
それらを考慮すると、非同期処理を使わない、同期処理中心の実装を行うことでアプリケーションの開発効率を向上させることができると思いました。
開発するプロダクトの性質によっては、システムのパフォーマンスよりも開発効率を重視することが望ましい場合もあるはずです。
そしてそれは、割とよくあるケースなのではないでしょうか。
サーバーサイド開発に利用される他の言語では、非同期処理を使わないことが一般的です。
必ずしも非同期処理を使ってパフォーマンスを追求せずとも、別の面でメリットがあれば採用されることも多いということです。
(むしろ現在はサーバーサイド開発にTypeScript/JavaScriptを選ぶことはまだ主流ではないように感じます。)
サーバーサイド開発にTypeScriptを選ぶメリット
ではパフォーマンスよりも開発効率を重視する場合、サーバーサイド開発にTypeScriptを選ぶ理由はあるのでしょうか。
私は、主に2つの大きな利点がその理由になると思います。
1. フロントエンドとの開発言語共通化による、コンテキストスイッチの軽減
現在、Webアプリケーションのフロントエンド開発にはTypeScriptが広く利用されています。
サーバーサイド開発にもTypeScriptを採用することで、フロントエンドとの共通化を図り、言語のスイッチングコストを下げることで開発者の負担を軽減することができます。
2. 型安全性、型のサポートによる開発体験の向上
TypeScriptは静的型付け言語です。それにより補完機能などエディタによるサポートも受けやすく、型チェックによりバグを早く検知することもできます。特に大規模なアプリケーションの開発においては型安全性は非常に強力な要素となります。
サーバーサイド開発に選ばれる他の言語は必ずしも型安全性が高いものばかりではないため、それと比較してTypeScriptを選ぶメリットは大きいと言えるでしょう。
この2つの利点があるため、開発効率を重視する場合でもサーバーサイド開発にTypeScriptを選びたいケースがあると思います。
サーバーサイドTypeScriptの開発効率をより向上させるためには
上記2点から、TypeScriptでの開発は開発効率を高く保てる特徴があります。しかし、非同期処理中心の実装を求められる現在のTypeScriptサーバーサイド開発環境は、その開発効率を下げる要因となっているように感じました。
またそれは各種ライブラリにも影響を与えていそうに思います。非同期処理が前提のために、必ずしも理想のインターフェースが採用できず、妥協した使い勝手になっていることもあるはずです。
結果として、サーバーサイド開発にTypeScriptを選ぶメリットが半減してしまっているのではないでしょうか。
サーバーサイドのTypeScript開発がより広く行われるためには、アプリケーションやライブラリの実装で非同期処理を使わない開発ができる、そのような選択肢が必要だと考えました。DBアクセスがあるたびに非同期処理をケアする必要がない、そのような開発体験が選べることが必要です。
私は新しいTypeScript用ORMライブラリを開発するにあたり、同期APIを採用することにしました。
その理由は、同期APIを使うことでよりインターフェースを抽象化でき、非同期APIを用いるよりもライブラリ利用者の体験が向上すると考えたからです。
いままでのサーバーサイドJavaScriptは、パフォーマンスで選ばれていたかもしれません。
しかしサーバーサイドTypeScriptは、開発効率で選ばれるのではないかと思います。そしてそれを後押しするためには、非同期処理を使わない選択肢が必要だと考えます。
まとめ
TypeScriptを使ったサーバーサイド開発の開発効率向上のためには、非同期処理を使わない開発ができる、という選択肢が必要なのではないかという考えを述べました。
Accel Recordが同期APIを採用することでどのようなインターフェースを実現できているか『Active Recordパターンを採用したTypeScript用ORM「Accel Record」の紹介』やREADME(日本語)で是非チェックしてみてください。
(この記事は『Even Server-Side TypeScript Needs the Option to Avoid Asynchronous Processing』の原文です)
-
非同期処理がORMのインターフェースにどのような影響があるかは、以前書いた『TypeScriptの新しいORMに同期APIを採用した理由』を参考にしてみてください。 ↩