Spring エコシステムにおいて、「リアクティブ(Reactive)」プログラミングモデルは主に Spring WebFlux によって実現される。従来の Spring MVC(Servlet API ベース)は本質的にブロッキングかつ同期的であり、リアクティブフレームワークではない。
したがって厳密には:
Spring MVC は真の意味でのリアクティブインターフェースをサポートしない。
✅ リアクティブな API を構築するには、Spring WebFlux を使用すべきである。
多くの開発者が「ストリーミング応答(SSE やファイルダウンロードなど)」と「リアクティブプログラミング」を混同しがちである。以下では、概念の明確化 → 比較分析 → 正しい実装例 の順で詳細に解説する。
1. 核心となる概念の整理
| 特性 | Spring MVC | Spring WebFlux |
|---|---|---|
| プログラミングモデル | ブロッキング・同期(Servlet) | ノンブロッキング・非同期・リアクティブ(Reactor) |
| スレッドモデル | リクエストごとにスレッド(Tomcat スレッドプール) | 少数スレッドで多数の同時接続を処理(Event Loop + 非同期 I/O) |
| リアクティブ型のサポート | Mono/Flux をコアとして扱わない |
ネイティブで Mono<T> / Flux<T> をサポート |
| ストリーミング出力 | SseEmitter, ResponseBodyEmitter などで対応可能 |
Flux を直接 SSE として出力可能 |
| 実行コンテナ | Servlet コンテナ必須(Tomcat, Jetty など) | Netty または Servlet コンテナで動作可能 |
重要な結論:
-spring-boot-starter-web(= Spring MVC)を使用している場合、戻り値にMono<String>を使っても、内部ではブロッキング実行される。
- 真のリアクティブ処理にはspring-boot-starter-webfluxが必要。
2. Spring MVC における「擬似リアクティブ」実装(高負荷環境では非推奨)
Spring MVC でも Mono や Flux を返すことは可能だが、バックプレッシャーやノンブロッキング I/O は有効にならない。単なる非同期ラッパーに過ぎない。
例:Spring MVC で Flux を返す(注意:WebFlux ではない)
@RestController
public class PseudoReactiveEndpoint {
@GetMapping("/pseudo")
public Flux<String> simulateStream() {
return Flux.just("X", "Y", "Z")
.delayElements(Duration.ofSeconds(1)); // Reactor API を使うが、Tomcat スレッドをブロック
}
}
実際の挙動:
- リクエストは Tomcat スレッドで処理される。
delayElementsはそのスレッドを約3秒間占有する。- リアクティブの利点は一切得られず、コードの複雑さだけが増す。
この実装はパフォーマンス向上に寄与せず、移行期や軽微な用途に限定すべきである。
3. 真のリアクティブ API:Spring WebFlux の活用
ステップ1:依存関係の切り替え
<!-- spring-boot-starter-web を置き換える -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
これによりアプリケーションはデフォルトで Netty 上で動作し、ノンブロッキングサーバーとして機能する(Tomcat/Jetty への設定も可能だが、一部の利点が失われる)。
ステップ2:リアクティブコントローラーの実装
例1:基本的な非同期レスポンス(HTTP 短時間接続)
@RestController
public class TrueReactiveApi {
@GetMapping("/hello")
public Mono<String> sayHello() {
return Mono.just("WebFlux へようこそ!")
.delayElement(Duration.ofMillis(100)); // ノンブロッキング遅延
}
@GetMapping("/countdown")
public Flux<Integer> emitNumbers() {
return Flux.interval(Duration.ofSeconds(1))
.map(i -> (int) i + 1)
.take(5); // 1 から 5 を1秒間隔で送信
}
}
例2:Server-Sent Events (SSE)
@GetMapping(value = "/live-updates", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamEvents() {
return Flux.interval(Duration.ofSeconds(2))
.map(seq -> ServerSentEvent.<String>builder()
.id(Long.toString(seq))
.event("update")
.data("Update #" + seq)
.build())
.take(3);
}
出力形式(自動的に data: プレフィックス付き):
data: Update #0
data: Update #1
data: Update #2
例3:リアクティブデータベースアクセス(R2DBC 連携)
@GetMapping("/customers")
public Flux<Customer> fetchAllCustomers() {
return customerRepository.findAll(); // ReactiveCrudRepository 実装
}
ステップ3:内部動作原理(Reactor + Netty)
- Reactor ライブラリ:
Mono(0〜1要素)、Flux(0〜N要素)を提供。- マッピング、フィルタリング、フラットマップなどのオペレーターと、バックプレッシャー制御をサポート。
- ノンブロッキング I/O:
- Netty の EventLoop が少数スレッドで数万の同時接続を処理。
- 外部通信には WebClient や R2DBC などのリアクティブクライアントが必要。
- エンドツーエンドのリアクティブチェーン:
Netty → WebFlux Controller → WebClient/R2DBC → 外部サービス/DB
全てのレイヤーがノンブロッキングで、スレッドの待機が発生しない。
4. 選択基準:MVC vs WebFlux
| ユースケース | 推奨アプローチ |
|---|---|
| 従来型 CRUD、Servlet に慣れたチーム | Spring MVC |
| 高スループット・I/O 負荷(API ゲートウェイ、リアルタイム通知) | Spring WebFlux |
| JPA/Hibernate を使用中の既存システム | WebFlux は不向き(JPA はブロッキング) |
| MongoDB / Cassandra / R2DBC を利用 | WebFlux |
| WebSocket や SSE による効率的なプッシュ配信 | WebFlux |
重要注意:
リアクティブは万能ではない。データベース、キャッシュ、外部呼び出しまで含めた全経路がノンブロッキングでなければ、性能劣化の原因となる(例:JDBC 使用やThread.sleep()の混入)。
5. よくある誤解
誤解1:「戻り値が Mono ならリアクティブ」
- Spring MVC では
Monoは内部でDeferredResultに変換され、結果を同期的に待機するため、依然としてブロッキングモデルである。
誤解2:「WebFlux は常に MVC より高速」
- CPU 負荷が高い処理では、スケジューリングオーバーヘッドにより逆に遅くなる可能性がある。
- I/O 負荷が高く、同時接続数が多い場合にのみ、WebFlux のリソース効率が優位になる。
正しい認識:
WebFlux の価値は「単一リクエストの速度」ではなく、「少ないハードウェアで大量の同時接続を安定処理できる点」にある。
6. まとめ
| 質問 | 回答 |
|---|---|
| Spring MVC でリアクティブ API は作れるか? | ❌ 不可。本質的にブロッキングモデル。 |
| 真のリアクティブを実現する方法は? | ✅ Spring WebFlux + Netty + リアクティブクライアント の組み合わせ。 |
| MVC で Flux を使う意味はあるか? | ⚠️ 構文上の糖衣に過ぎず、パフォーマンス向上なし。 |
| WebFlux を採用すべきタイミングは? | 高同時接続・I/O 密集型で、かつ全経路がノンブロッキング対応済みの場合。 |