Back
Featured image of post Spring WebFlux、少し触ってみてからの話

Spring WebFlux、少し触ってみてからの話

今まではずっとSpring MVCを使ってきたので、最近はKotlin + Spring WebFluxという組み合わせで簡単なアプリを作ってみているところです(Spring WebFluxそのものについての紹介は、前回のポストでしているのでここでは割愛します)。Spring WebFluxが紹介されたのももう3年前のことなので(あと少しで、4年になりますね)、もうかなりの時間が経ちますが、実際はあまり幅広く使われてはいないのが現状ではないかと思います。なのでネットで調べてもあまり参考できそうなものがなかったりしますね。

これはおそらくSpring WebFluxが本格的に導入するにはまだ色々と考えるべきところがあるからでしょう。例えば、フレームワークとしてまだまだ成熟している技術ではないということがあります。まだ、このフレームワークの核心となるReactorの書き方に慣れていない人が多い(Reactiveといいつつ、RxJavaともまた微妙に違いますしね)という面も考慮しなくてはならないです。会社の立場からしたら、このようにまだ新しい技術をすぐに取り入れるということはリスクもあり、エンジニアの学習コストも考えなくてはいけないという面からあまりメリットがないです。

そして前回紹介した通り、パフォーマンスの面からしても既存のSpring MVCプロジェクトをSpring WebFluxに変えるだけではあまり得しないという問題があります。なら、新規プロジェクトから導入して良いのでは?と思われる可能性もあると思いますが、スタートアップやベンチャー企業などではそもそもJVM言語を使和ないケースが多いので(勝手なイメージかも知れませんが、そのような企業はやはりPythonやRuby、JavaScriptが多いと思います)、そもそも考慮の対象になっていないのかも知れません。また、もしJVM言語に慣れているエンジニアがいるからSpring WebFluxは導入できるとしても、やはり前述した「検証されてない」と「学習コストがかかる」という問題からは自由ではないですね。

少なくともこれらの理由からエンタープライズレベルでは、まだSpring WebFluxの導入は難しいかと思います。ただ、おそらくSpringの未来はWebFluxにあるので、これからだんだんWebFluxを中心に開発が行われる可能性もあるので、今からでもReactorの書き方になれる必要があるのかも知れません。また、Springという一つのフレームワークの観点からでなくても、非同期による同時処理性能の向上は、多数のユーザの利用する頻度の高いWebアプリケーションの開発においては重要な要素であるので、少なくともその概念、思想、そしてコードの書き方には慣れる必要があるかも知れません。そういう意味で、自作アプリをSpring WebFluxで書いてみた経験とそこから感じたことを述べたいと思います。

Spring WebFluxのアプリを書くということ

Spirng WebFluxは既存のController + Serviceというパターンでもコードを書くことができます。なので、一見みると既存のSpring MVCで作られたプロジェクトと並行して運用したり、既存のコードを少し書き換えるだけで簡単にWebFluxに移行できそうなイメージを与えていると思います。が、実際はそうでもないような気がします。まず私の場合、簡単なCRUDのサンプル(GitHubのリポジトリはこちら)を作ってみてから、これを応用して調整さんのマイナークロンを作ってみようとしました。

ここで、サンプルではSpring Data JPAを使っていましたが、WebFluxで本格的なノンブロッキングのアプリを書いてみたいと思ったので、R2DBCを導入してDBアクセスも非同期で構成することにしました。なぜかというと、非同期の処理の中に一つでも同期の処理が発生するとしたら、それだけでももはや非同期ではなくなるからです。なのでORMもそれに合わせてR2DBCを使う必要がる、とのことです。

幸い、R2DBCは使い方としてもSpring Data JPAやSpring Data JDBCとそう変わらない感覚で、Interface形式のRepositoryを作り、DTO形式のオブジェクト(Kotlinなら、Data classで十分でした)を作るだけでDBとのマッピングは簡単にできました。あとJPAに比べ、アノテーションの数が少なくなっているだけなのでテーブルとしてオブジェクトを定義するのも簡単です(@Idをつけるのみで終わります)。そしてSpring Data JDBCとは違って、メソッド名から自動でクエリを生成してくれるというところもあったので、最初は楽だと思いました。でも、やはり初めて触っている技術で、そうなんでもうまくいくはずがありません。一つ、問題にあってしまいました。

Joinができない

個人的にはどんな技術でも、やはりある程度の時間がたち、安定期に入る前までは既存の技術に比べ圧倒的な優位にあり、すぐに乗り換えるべきと言えなくするところ(つまり、レガシーを捨てがたくする要素)が必ずしも一つ以上は存在すると思います。そういう意味からすると、Spring Data R2DBCが既存のORMを今すぐ代替するには十分ではないと言える部分は、自動でJoinを行う方法がない(テーブル間の関係をあらかじめオブジェクトとして定義することができない)というところと言えるのではと思います。

Spring Data JPAやJDBCを使う場合、アノテーションを使うことで簡単にテーブル間の関係(@OneToOneなど)を定義できて、テーブルの関係をコード内で簡単に定義できます。しかし、これがまだR2DBCでは対応していない機能となっています。こういう状況では、リポジトリに@Queryアノテーションをつけて直接Joinが含まれたSQLを書いたメソッドを定義するか、二つのオブジェクトをアプリケーションの中で組み合わせるかの方法があるかと思います。

ここで前者の場合、コードカバレッジとして取れない部分になってしまうので(そして、個人的には性能が良いとしても、あまりクエリが複雑になりそうなものはメンテの観点からよく思ってないので)、後者の方法を取ることにしました。オブジェクトとリポジトリが1:1になって、後でテーブルに修正が発生してもそのテーブルに当てはまるオブジェクトを直すだけで済むので、より簡単な方法だと思ったからです。しかし、その決定にも問題はありました。R2DBCが返すSQLの実行結果としてのオブジェクトは、オブジェクトそのままではなく、MonoFluxであったからです。

block()のジレンマ

リポジトリから取得したオブジェクトがMonoFluxなので、またどうやって二つのオブジェクトを組み合わせるか(Joinさせるか)を考える必要があります。

一番簡単な方法としては、MonoFluxをブロッキングして使う方法がありますね。すでにMonoFluxにはblock()というメソッドが用意されてあって、同期のコードの中で使うことも可能になっています。例えばSpring MVCでもRestTemplateよりWebClientを使うことが推奨されているので、非同期と同期のの共存が不可能なわけでも、おかしいわけでもないです。

ただ問題は、そのような方法を取ると非同期のメリットがなくなるということです。なぜなら非同期が見せてくれる素晴らしい同時処理性能は、どこか一つの箇所でもブロッキングが挟むと、結局は同期コードになってしまうからです。それなら今までのSpring MVCとORMでよくて、あえてWebFluxやR2DBCを使う必要が無くなりますね。なので別の方法を試してみることにしました。

Mono + MonoもしくはFlux + Flux

やはりここで取るべき方法は、非同期に相応しい処理方法を探すことでしょう。なので調べた結果、二つのMonoもしくはFluxを配列のようなオブジェクトとして結合して扱う方法があるということがわかりました。答えは意外と簡単で、zipWith()というメソッドを使うことで二つのMonoFluxをつなげることができます。つなげたMonoもしくはFluxは、Kotlin基準でタプルになるのであとはmap()からタプルのインデックス(繋ぎ元がt1、繋ぎ先がt2となります)を指定して使うだけです。例えば、以下のコードはPaticipantというオブジェクトをリポジトリから取得したあと、さらにCandidateParticipantsを取得して結合する例です。

fun getParticipant(participantId: Long): Mono<ParticipantDto> =
    repository.findById(participantId)
        .zipWith(candidateParticipantHandler.getåCandidateParticipantsByParticipantsId(participantId).collectList())
        .map { mapDto(it.t1, it.t2) }

ただ、このような方式を使ってテーブルをJoinするためには、二つのMonoとFluxを取得できる環境である必要があります。なのでSelectを発行するメソッド(Get系のAPIなど)なら、オブジェクトにテーブル間の関係を上手く設計して反映する必要があります。つまり、一つのキーでJoin対象の全てのオブジェクトを取得できるようにする必要があるということになります。最初のオブジェクトを取得したあと、そのオブジェクトが持つまたのキーで紹介したら良いのでは?と思ったこともありますが、残念ながら私の知る限りは簡単にできそうにないです。なぜなら、そのような方法を取るには以下の手順が必要になるからです。

  1. キーを持ってJoin元のテーブルを照会、データをMonoもしくはFluxとして取得する
  2. 取得したMonoもしくはFluxmap()を呼び出し、更なるキーを抽出、Join先のテーブルを照会する
  3. Join元とJoin先のテーブルを組み合わせる

一見問題なさそうですが、Join先のテーブルを紹介する段階で、またのMonoもしくはFluxを取得してしまうので、それからどうやって元のオブジェクトを取り出すかが問題となります。ここでまたblock()を使うと、今までやってきたことたちが台無しとなってしまうわけです。なので、かなり不便でありながら、コードを持ってのテーブルのJoinは、現時点ではこのような方法しかないのではと思っているところです。

これがベストか

こうやって、WebFluxで疑問となった問題は、なんとか解決することはできました。しかし、個人的にはこのようなやり方に違和感があります。そして、その理由をProject LoomのリーダであるRon Pressleの説明から探すことができました。彼の話によると、今の非同期プログラミングは以下の三つの問題を持っています。

コントロールフローを見失いやすい

非同期でコードを書いていると、どんなロジックと目的でコードを書いていたか忘れてしまうような気がする時があります。それはおそらく、アプリケーションの本来の目的を達成するための「ビジネスロジック」よりも、「ノンブロッキングのお作法」の方を気にかけることが多くなるからですね。非同期でコードを書いていると、簡単な条件分岐や繰り返してもかなりコードが複雑になり、一体どのような処理をしようとしたか、その制御の流れを見失いやすくなります。Javaだけやっていた自分にはあまり実感がないですが、JavaScirptで非同期のコードを書いた経験のある方にはこれが理解できるでしょう。(あの有名な、コールバックヘル問題とかがあるし…)

例えばこのポストでも紹介した通り、二つのテーブルをJoinするために、何をしているかをみてください。同期だったら、最初からJoinしたデータを取得するか、二つのオブジェクトを順番に宣言して処理するだけ済む話ですね。このように簡単な処理でも非同期に変えようとすると、そのコードで何をしたいかよりも、まず非同期の形式に合わせたコードを書くことになるので、「一体何をしようとしているのか」という、そもそもの目的がわからなくなる場合があるという話です。

コンテキストを見失ってしまう

非同期だと、例外が発生した時に、スタックトレースを追うのが非常に難しくなります。なぜなら、同期の場合は一つのスレッドが一つのリクエストを処理するため、何が実行されどんな結果になったのかを追跡するためにはそのスレッドの残した履歴を見るだけで十分です。しかし、非同期だと、一つのリクエストが複数のスレッドを渡りながら処理されるため、一つのスレッドの履歴を追うだけでは一体どんなことが起こっているかわからなくなるからです。

コードの伝染

非同期でコードを書くことになると、結局はアプリケーション全体から同期という概念を排除する結論に至るかも知れません。なぜなら、先に述べました通り、非同期の処理の中で一つでも同期の処理が混ざっているとしたら、それだけでも全体の処理が同期になっちゃうからです。なので、同期と非同期を一つのアプリの中で共存させるのはかなり難しくなり、結果的には非同期のコードに他のコードが「伝染」されるようなことが起きてしまうケースが発生します。例えばWebFluxの例では、同期のコードと混ぜて使うこともほぼ不可能に近いので、あえてオブジェクトをMonoFluxに入れ(非同期に変え)、zipWith()でタプルとして結び、map()flatMap()で処理するという形になるしかなくなります。そしてその逆の方法を取るとしたら(block()Monoの中身を持ち出すなど)、もはや最初から同期でコードを書いた方が良いということになってしまうという問題があります。

それでも非同期は必要

以上のことは、おそらく非同期に触れてみた人なら誰でも一度は触れてみた問題であり、共感する人も多いのではないかと思います。しかし、このような不便さがあるにもかかわらず、依然として非同期プログラミングの必要性はあります。特に、今のトレンドだと多くのWebアプリケーションで同時処理性能が重要となっていますので、尚更です。実際グーグルアナリティクスは、KissMetricsを引用して「ページのレスポンスが 1 秒遅れると、コンバージョン数が 7% 減少する」、「47% の消費者は 2 秒以内にウェブページが読み込まれることを期待している」と行っているくらいですが、このような要求に対応できるのはやはり非同期としか言えません。なので、JavaScriptやC#のような言語ではasync/await/promiseなどを試し、KotlinでCoroutineというものを導入するなど、なんとしても非同期の短所を補完するという努力をしているところですね。

特に、Javaの場合はOSのスレッドを直接使用するので、同時に処理できるリクエストは数千くらいにすぎません。なので、このスレッド基盤という言語そのものの限界を克服するため今まで多くの非同期ライブラリが作られてきました。しかし、ライブラリにはやはり限界があったので、JVMレベルでの対応が検討されているところです。それが先ほど紹介しました、Project Loomというものです。

Project Loomという答え

Project Loomでは、既存のスレッドをFiberという仮想・軽量スレッドとして切り分け、同時処理性能をあげると同時に、「非同期プログラミングを同期プログラミングの感覚でできるように」して、今までのように非同期か同期かによってコードが変わるような現象を無くすのが目的です。Fiberは数百万まで生成できるというし、使い方としても既存のスレッドどあまり変わらないので、コードの修正も少なくなります。それに、同期の感覚で書いても内部的にasync/awaitを使ったかのように処理されるので、コールバックヘルのような問題も無くなりますね。

また、KotlinのCoroutineはあくまでコンパイラレベルの対応なので、上述した3つの問題のうち、「コンテキストを見失う」、「コードの伝染」という問題は根本的に解決できませんが、Project LoomはJVMレベルの対応なので、これらの問題が全部解消されるというメリットもあります。そして既存の同期APIを非同期に変えることで、クライアントコードとしてはあまり変わることなくすぐに適応できるのも魅力的ですね。

多くの場合に「非同期の性能」は欲しくても、多くのプログラマが「同期の書き方」に慣れているという現実からして、Proejct Loomが正式リリースされるとしたら既存のCoroutineやasync/await/promise、Reactice Streamを使ったプロジェクトの多くがFiberを使ったコードに変わるのではないかと思います。

最後に

どんな分野でも、過渡期にあるものがもっとも混乱しやすく、辛いものです。そしてプログラミングにおいては、非同期プログラミングというパラダイムが、まさにそうなのではないかと思います。多くの天才たちが言語、ライブラリ、フレームワークとしうさまざまな方面から非同期プログラミングの短所を克服しようと努力してきて、やがてその結実が見えてくるような気がします。Reactive Steamの思想、「反応型」という概念は個人的に嫌いではないですが、それが今までのプログラミングに比べあまりにも変わった書き方になってくるので、個人的には「特定の目的のため、完全に違う言語も学ぶのと同じくらいの努力が必要となるが、その効率の悪さを甘んずるほどのメリットがあるか」とも思います。それはおそらく、まだ自分が本格的に非同期のコードを書いてみたことがないのでそう思うだけなのかも知れませんが。でも、これもProject Loomのように、同期の感覚で非同期プログラミングができるような技術があると解決される問題でしょう。

ただ、まだProject Loomは完成した技術ではなく、開発途中のものなので、のちに限界や問題が見つかる可能性もありますね。そして将来的にWebFluxなどで採用されるとしても、リリースのタイミングがわからないので、それまではReactive StreamやCoroutineを使うしかないので、のちにLoomのリリースに合わせて既存のコードを全部書き換えるというのも無理な話です。なるべく早く出て欲しいのですが、まだ2年以内のリリースはないみたいなので、今の姿とは全く違うものになる可能性もありますね。

だとしても、JVM自体が代わり、非同期プログラミングがより楽なるということは魅力的なものです。最近はKotlinにハマっていますが、こうやって変わっていくJavaを見るのも楽しいことですね。また新しい世界が見えてきそうな気分です。

では、また!

Built with Hugo
Theme Stack designed by Jimmy