この記事はSRA Advent Calendar 2022の1日目の記事です。
こんにちは! 関西事業部の佐々木です。
Reactiveプログラミングとは
Reactiveプログラミングを勉強しようと思って「Reactiveプログラミングとは」などのキーワードでWeb検索するとたくさんの記事がヒットします。
ただ、それら記事をあまり前提知識のない状態で目を通しても理解するのは難しいのではないでしょうか。
値の変更を伝播させるデータフロー指向のプログラミングパラダイム
や
値の関係性を記述してプログラミングする
などと説明されて一発でわかる人はいいですが、何のことかよくわからない人もいると思います。
他にも「Reactiveプログラミング」と「関数型Reactiveプログラミング」の違いだとか難しい説明がいくらでも出てきます。
Reactiveプログラミングを本質から理解することは、それはそれで重要でしょうが、具体的なプログラミングを通して徐々に慣れていくことで本質に近づいていくという方法の方が向いている人もいるに違いありません。
そこで本記事では、Reactiveプログラミングの本質は置いておいて実装するときに最初に理解しておくべきポイントに絞って解説いたします。
とは言え、Reactiveプログラミングのメリット(の一部)を理解していないと実装の意味もわからないので簡単にReactiveプログラミングのメリット(の一部)を紹介します。
上記でしつこく書いているように、以下のメリットはReactiveプログラミングのメリットの一部の話です。
また、本記事ではSpring WebFluxを使ってReactiveプログラミングを実装することを想定しています。
ReactiveプログラミングはCPU資源を有効活用できます
Reactiveプログラミングで正しく実装すると ノンブロッキングで非同期 なプログラミングとなり、 待ちが発生したスレッドは別の処理(別のリクエスト)を処理 することができます。
例えば、Webアプリであるリクエストを受け付けてその内容をDBに保存し、その後DBを検索して結果を返すという処理をするとします。もしもノンブロッキングでなかったり非同期でない場合、あるリクエストを処理しているスレッドはDBに保存してその結果がDBから返ってくる間や、DBを検索してその結果を受け取るまでの間、単なる 待ち の状態で何も処理をしません。これはこれでSpring Bootなどの一般的なWebアプリでよくある動きなのですが、同時に複数のリクエストを捌くにはその分のスレッドが必要となってきます。(基本的に1リクエスト1スレッドが必要)
一方、ノンブロッキングで非同期なプログラミングの場合、DBからの応答待ちの間に他のリクエストの処理を行ったりします。その結果、少ないスレッド数で多くのリクエストを同時に捌くことができます。
最近はやりのマイクロサービスなどで小さいPodで運用したい場合などでは特に有効になると思います。
Reactiveプログラミングの実装で最初に理解すべきところ
まずは次のメソッドを見てください
public Mono<User> query(ReactiveCrudRepository<User, Integer> repository, Integer id) {
return repository.findById(id);
}
Reactiveプログラミングに対応したのRDB用ライブラリに R2DBC というのがあり、Spring Dataでもサポートされています。
上記の ReactiveCrudRepository<User, Integer> repository
とうのはR2DBCのinterfaceです。
(UserというのはRDBのレコード格納用Entityです)
ここではR2DBCについての詳しい説明は省略させてもらいますが、
repository.findById(id)
でDBの検索が実行されます。
そして注目してほしいところは、このメソッドのreturn値が Mono<User>
となっているところです。
Reactiveプログラミングを実装しているとわかりますが、このMonoまたはFluxというのと格闘することになります。
MonoとFluxについてはMonoが単数なのに対し、Fluxが複数という違いがあるだけなのでここではMonoに絞って説明いたします。
上記の repository.findById(id)
がMonoをreturnするということですが、このMonoはDBの検索が終わってなくても即座に返ってきます。この 検索が終わってなくても というのがミソで、ノンブロッキングで非同期な処理を実現することができます。
それではこのMonoが返ってきた後の実装ですが、仮に検索結果で取得したUserの中の name
というプロパティだけを返すような処理の場合、下記のような実装が考えられます
public Mono<String> getUserName(ReactiveCrudRepository<User, Integer> repository, Integer id) {
return query(repository, id).map(user -> {
return user.getName();
});
}
このサンプルコードは実際にはメソッド参照でもう少し簡潔に書けるのですが、説明しやすいようにベタに書いてます。
getUserNameメソッドではひとつ前のコード例のqueryメソッドを呼んでいます。先ほど説明したようにqueryメソッドはMonoを返しますが、ここではMonoのmapメソッドを実行しています。ここではラムダ式で書いてますが、mapメソッドの引数にはFunctionオブジェクトを指定します。
Functionへの引数には検索結果のUserオブジェクトが渡ってくるのでそのUserのgetNameでnameの値を取得して返します。nameはString型なのでgetUserNameメソッドのreturn値は Mono<String>
となります。
さらっと説明してしまいましたが、mapのreturn値によってこのメソッドのreturn値がMonoになっているところが本記事のコアの部分です。
この部分は一旦置いておいて、先ほど 検索が終わってなくても queryを実行するとMonoが返ってくると言ったところの説明をいたします。
queryのMonoは即座に返ってくるのでmapメソッドはすぐに実行されます。
しかし、mapメソッドの引数に指定したFunctionオブジェクトは DBの検索結果が返ってきたら実行 されます。そしてこの検索結果が返ってくるまで、つまりmapメソッド内のFunctionが実行されるまで、このメソッドを実行していたスレッドは別の処理を行うことができます。
このようにしてReactiveプログラミングではノンブロッキングで非同期な実装により少ないスレッドで複数の処理を同時に実行することが可能となります。
FlatMapは一皮むく
先ほど「mapのreturn値によってこのメソッドのreturn値がMonoになっているところが本記事のコアの部分です」と述べましたが、Reativeプログラミングで特にWebアプリの実装をする時にこのmap(またはflatMap)の使い方につまずく人が多い印象です。
ネット上ではReactiveプログラミングの例として doOnNext などの実装が良く見られますが、Webアプリの場合最終的にResponseを返すことになるのでmap(またはflatMap)の連続になることが多いのではないでしょうか。
イメージとしては
public Mono<GetUserNameResponse> getUserName(Mono<GetUserNameRequest> request) {
return request.flatMap(req -> {
return loginService.fromLoginNameToUserId(req.getLoginName()).flatMap(userId -> {
return userService.getUserName(userId).map(userName -> {
return new GetUserNameResponse(userName);
});
});
});
}
という感じです。
このコードはツッコミどころはいくつかありますが、一旦イメージをつかむためのものだと思ってください。
requestがMonoで渡ってくるのでflatMapで(Monoから取り出した)GetUserNameRequestオブジェクトを使い、loginServiceのfromLoginNameToUserIdメソッドを呼び出し、その戻り値のflatMap
メソッドでさらにuserServiceのgetUserNameを呼び出し、その戻り値のmapメソッドを呼び出しています。
このようにmapメソッドとflatMapメソッドがいくつも登場することがありその呼び分けで混乱する開発者を何人か見てきました。
個人的な意見ですが、その混乱はmap(またはflatMap)メソッドが呼ばれるオブジェクトでなくその引数に渡されるFunctionオブジェクト(上記例ではLambda式)のreturn値によって呼び分ける必要があるためではないでしょうか。
具体的には return値がMonoで包まれているときはflatMap、そうでない場合はmap を使うということです。
上記の説明は本質的ではありません。本質的には「同期でemitされる場合map」「非同期でemitされる場合flatMap」となりますが、理解しにくい場合は一旦上記の説明で理解してください
例えば最初の request.flatMap
ですが、この場合はLambda式の中でreturnされるオブジェクトは loginService.fromLoginNameToUserId
のreturn値で、これがMonoなのでflatMapを使っています。(サンプルコード内には loginService.fromLoginNameToUserId
はありませんが、Monoを返すことを想定しています)
逆に userService.getUserName(userId)
ではLambda式が返す値がMonoではなく単なるGetUserNameResponseオブジェクトなのでmapを使っています。
最終的に返す値が Mono<Mono<String>>
などのように入れ子になったMonoとかでない限り、Functionオブジェクト(上記サンプルではLambda式)が返す値がMonoの場合はflatMapメソッドでMonoという 皮をむいて あげる必要があります。Monoにくるまれていない場合はそのままmapメソッドを使えばいいというわけです。
Reactiveプログラミングを実装していて、mapとflatMapの使い分けにイマイチ納得していないという人に本記事が少しでも役に立てれば幸いです。
納得いくまでどんどんプログラミングをして 一皮むけた プログラマーを目指しましょう!!