「WebFluxという言葉は聞くけど、Spring MVCと何が違うの?」というのはよくある疑問ですよね。概念の説明だけ読んでもピンとこないことが多いので、この記事では なぜ必要か から始めてコードを動かしながら理解できるように進めます。
リアクティブプログラミングとは何か
Spring MVCは 1リクエスト1スレッド で動きます。DBアクセスや外部API呼び出しなどのI/O処理中、スレッドはただ待っているだけです。リクエストが増えればスレッドも増やすしかなく、スレッドプールが枯渇すると新しいリクエストを受け付けられなくなります。
ノンブロッキングI/Oはこの「待つだけ」を解消するアプローチです。I/O処理をOSに委ねている間に、同じスレッドで別のリクエストを処理できます。少ないスレッドで大量の並列リクエストをさばけるのが特徴です。
リアクティブプログラミングはこのノンブロッキングI/Oと相性の良いプログラミングスタイルで、データを ストリーム として扱い、処理を宣言的に連鎖させます。Spring WebFluxの基盤ライブラリは Project Reactor で、Mono と Flux という2つの型がその核心です。
Spring MVCとWebFluxの比較
どちらが優れているという話ではなく、向き不向きがあります。
| 項目 | Spring MVC | Spring WebFlux |
|---|---|---|
| 処理モデル | ブロッキング・同期 | ノンブロッキング・非同期 |
| サーバー | Tomcat(デフォルト) | Netty(デフォルト) |
| スレッド数 | リクエスト数に比例 | 少数(CPUコア数程度) |
| プログラミングモデル | 命令型 | リアクティブ・関数型 |
| 主な用途 | 一般的なWebアプリ、CRUD | 高並列I/O、ストリーミング |
WebFluxが向くのは、外部APIやDBへのI/O待ちが多い APIゲートウェイ 、 マイクロサービス間通信 、リアルタイムデータの ストリーミングレスポンス などです。一方、JDBCを使ったRDBMS操作が中心のアプリや、チームがリアクティブに不慣れな場合はSpring MVCのままで十分です。
セットアップ
spring-boot-starter-webflux を追加するだけです。spring-boot-starter-web(Tomcat)との共存は避けましょう。同時に入っているとNettyとTomcatが競合します。
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
// spring-boot-starter-web は追加しない
}
application.properties は最小限でOKです。起動ログに Netty started on port 8080 と出ていれば成功です。
Mono - 0または1件のストリーム
Mono<T> は最大1要素を非同期に返すコンテナです。Spring MVCで言うと CompletableFuture<T> に近いイメージですね。
// 基本的な生成と変換
Mono<String> mono = Mono.just("Hello")
.map(s -> s.toUpperCase()) // 同期的な値変換
.flatMap(s -> Mono.just(s + "!")); // 別のMonoを返す非同期処理のチェーン
// 空やエラーのケース
Mono<String> empty = Mono.empty();
Mono<String> error = Mono.error(new RuntimeException("Something went wrong"));
map() は同期的な値変換、flatMap() は別の Mono を返す非同期処理のチェーンに使います。
重要な注意点 として、block() を本番コードで使ってはいけません。block() はリアクティブチェーンを破壊してスレッドをブロックしてしまい、WebFluxのメリットを完全に打ち消します。テストコード限定で使うものと覚えておきましょう。
Flux - 0件以上のストリーム
Flux<T> は0件以上の要素を非同期に流すストリームです。
Flux<String> flux = Flux.fromIterable(List.of("Apple", "Banana", "Cherry"))
.map(String::toUpperCase);
// 先頭2件だけ取得
Flux<String> top2 = flux.take(2);
// まとめてリストに変換
Mono<List<String>> listMono = flux.collectList();
take() で件数を絞ったり、collectList() で全要素をまとめたりできます。ただし collectList() は全要素をメモリに蓄積するので、大量データには注意が必要です。
RouterFunctionでエンドポイントを作る
WebFluxには @RestController の他に、 RouterFunction という関数型スタイルのルーティングAPIがあります。ルーティング定義と処理ロジックを分離できるのが特徴です。
@Configuration
public class UserRouter {
@Bean
public RouterFunction<ServerResponse> routes(UserHandler handler) {
return RouterFunctions.route()
.GET("/users/{id}", handler::getUser)
.POST("/users", handler::createUser)
.build();
}
}
@Component
public class UserHandler {
public Mono<ServerResponse> getUser(ServerRequest request) {
String id = request.pathVariable("id");
User user = new User(id, "Alice");
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(user), User.class);
}
}
@RestController のアノテーション方式もWebFlux環境でそのまま使えます。チームがアノテーション方式に慣れているなら、まずはそちらで始めても問題ありません。
WebClientについて
WebFlux環境でHTTPリクエストを送るなら WebClient を使います。従来の RestTemplate はブロッキングなのでWebFlux環境では非推奨です。
WebClient client = WebClient.create("https://api.example.com");
Mono<String> result = client.get()
.uri("/items/1")
.retrieve()
.bodyToMono(String.class);
WebClient の詳しい使い方は RestTemplate・WebClient比較ガイド をご覧ください。
WebFluxを採用すべきか
向くケース
- 外部APIへのリクエストが多いAPIゲートウェイ
- マイクロサービス間通信が頻繁
- サーバーサイドSSEやWebSocketが必要
- ストリーミングレスポンスを返したい
向かないケース
- JDBCを使ったRDB操作が中心(R2DBCへの移行コストが高い場合も多い)
- チームがリアクティブプログラミングに不慣れ
- 既存のSpring MVCアプリへの部分導入(Nettyへの切り替えが伴うため非推奨)
Spring MVCとWebFluxは プロジェクト単位で選択 するのが基本です。同一プロジェクトに混在させるとNetty/Tomcatの競合が起き、想定外の動作につながります。
非同期処理という観点でSpring MVCの @Async と比較されることもありますが、スレッドモデルや適用範囲が異なります。Spring Bootの非同期処理ガイド も合わせて参考にしてみてください。
まとめ
WebFluxは「Spring MVCの置き換え」ではなく、高並列I/O処理に特化した別の選択肢です。Mono/Flux の基本操作とRouterFunctionの書き方を押さえれば、意外とすんなり使い始められます。
まずはシンプルなGETエンドポイントを1本作ってみて、ストリームの流れを実際に体感するのがおすすめです。DBを絡めた実装に進む際はR2DBCの対応状況を確認することもお忘れなく。