「WebFluxという言葉は聞くけど、Spring MVCと何が違うの?」というのはよくある疑問ですよね。概念の説明だけ読んでもピンとこないことが多いので、この記事では なぜ必要か から始めてコードを動かしながら理解できるように進めます。

リアクティブプログラミングとは何か

Spring MVCは 1リクエスト1スレッド で動きます。DBアクセスや外部API呼び出しなどのI/O処理中、スレッドはただ待っているだけです。リクエストが増えればスレッドも増やすしかなく、スレッドプールが枯渇すると新しいリクエストを受け付けられなくなります。

ノンブロッキングI/Oはこの「待つだけ」を解消するアプローチです。I/O処理をOSに委ねている間に、同じスレッドで別のリクエストを処理できます。少ないスレッドで大量の並列リクエストをさばけるのが特徴です。

リアクティブプログラミングはこのノンブロッキングI/Oと相性の良いプログラミングスタイルで、データを ストリーム として扱い、処理を宣言的に連鎖させます。Spring WebFluxの基盤ライブラリは Project Reactor で、MonoFlux という2つの型がその核心です。

Spring MVCとWebFluxの比較

どちらが優れているという話ではなく、向き不向きがあります。

項目Spring MVCSpring 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の対応状況を確認することもお忘れなく。