R2DBC と Repository パターンについての現時点での評価と所感
R2DBC もだいぶ成熟してきたのかなと思い使ってみたところ、ちょっと思ってたのと違ったので、そのメモ。
結論から。誤解を恐れずに端的に言うならば、DDD を意識したプロジェクトの場合、R2DBC (Reactive Programming) は現時点で採用に値しない。 これに尽きます。
R2DBC だけではなく、Reactive Programming 全体にまで言及しているのは、R2DBC を採用せず、WebFlux だけ採用してもリソース効率の最適化といった点において、あまり大きな効果を発揮できない (犠牲になる保守性に対して、採用するだけの価値が見出だせない) ためです。
採用に値しないとまで断じた理由を以下、メリット・デメリットを挙げながら確認していこうと思います。
メリット
わたしの理解している Reactive Programming 最大のメリットとは、リソース効率を最適化すること です。
処理の遅延評価を進め、パイプライン化を促進することで、限られたリソースを有効に活用できるようになり、アプリケーションの高いパフォーマンスを可能な限り維持し続けられるようになる、というものです。ここでいう限られたリソースとは、スレッド間で共有しているすべてのリソースを指し、代表的なものであれば以下が挙げられるかと思います。
- CPU 資源
- メモリ資源 (ヒープ)
- コネクション
- ファイルディスクリプタ
これらのうち、CPU 資源やメモリ資源については、実行環境をスケールアウトすることでいくらでもキャパシティを増やすことができるでしょう。(オーバーヘッドの蓄積により、いつか限界を迎えるとはいえ。)
しかしコネクションなどについては、比較的すぐに上限を迎えやすいものであることは理解しやすいかと思います。こういったスケールアウトの難しいリソースの利用効率を高めることこそ、Reactive Programming の価値と言えます。
そういった意味で、R2DBC はリソース効率の最適化にとくに効果があるといえるでしょう。
しかし、この R2DBC にはひとつの大きな欠点があります。
デメリット
R2DBC 最大のデメリットは Flux
, Mono
に、Collection
, Optional
との互換がなく、Repository パターンが破壊されること にあります。
Repository パターンは、データアクセスを抽象化し、ドメイン層やアプリケーション層から、物理的なデータアクセスの都合や実体を隠蔽するものです。
しかし、R2DBC を利用した場合、Flux
や Mono
といったインターフェースを、ドメイン層やアプリケーション層まで引き渡す必要があります。ここで前述の 物理的なデータアクセスの都合や実体を隠蔽する、という Repository パターンにおける要件との矛盾 が生じます。
もしデータアクセスの都合や実体を隠蔽するという要件を適えるのであれば、List
や Optional
での返却が求められ、これを実現しようとした場合、Flux
や Mono
に対して block()
操作を呼び出し、List
や Optional
を取り出す必要があります。
ところが block()
操作を行った時点で、インスタンスは評価され、ノンブロッキング状態が損なわれることになります。せめてトランザクション境界までノンブロッキング状態を維持できれば、パフォーマンスの最適化にも多少は寄与できるでしょうが、Repository の実装内部で block()
操作をしなければならないとあれば、はじめからブロッキングな処理をするのとほとんど変わりがありません。
例を挙げてみるとわかりやすいでしょう。
R2DBC を使った場合のコード例は以下のようになります。
databaseClient
.execute(selectSql)
.bind(paramName1, paramValue1)
.bind(paramName2, paramValue2)
.fetch()
.all()
.map {
val selectItem1 = it[selectItemName1] as String
val selectItem2 = it[selectItemName2] as String?
val selectItem3 = it[selectItemName3] as Int
SomeClass(
selectItem1,
selectItem2,
selectItem3
)
}
.collectList()
.block() // List<SomeClass>?
一方、同様の結果を得ようとした場合の JDBC でのコード例は以下です。
namedParameterJdbcOperations
.query(selectSql, mapOf(
paramName1 to paramValue1,
paramName2 to paramValue2
))
.map { resultSet, _ ->
val selectItem1 = resultSet.getString(selectItemName1) // String!
val selectItem2 = resultSet.getString(selectItemName2) // String!
val selectItem3 = resultSet.getInt(selectItemName3) // Int!
SomeClass(
selectItem1,
selectItem2,
selectItem3
)
} // List<SomeClass>
このように、通常の JdbcOperations
や、NamedParameterJdbcOperations
を使用した場合とは異なり、Flux
や Mono
に対して追加でブロック操作をする必要があり、Flux
や Mono
での集計操作はコレクションクラス群ほど充実したものは用意されていません。すると、求められる操作は増え、できることは従来の実装と大差がないという、採用意図のよくわからない技術となってしまうのです。
これらのことから、R2DBC の採用が効果的といえる場面はまだ局所的なもので、長期的な保守が前提となっているシステムやプロジェクトへの採用としては、DDD などのアプローチによる保守性の向上を優先し、リソース効率の最適化に対する優先度は相対的に下がるのではないかと考えており、現時点ではまだ採用を見送るべき、と見ています。
もし、リソース効率の最適化が優先すべき要件であり、比較的シンプルな実装や、小さなプロジェクトサイズで済むような場合には、Reactive Programming を積極的に採用していってもよいのではないか、というのが現時点でのわたしなりの評価です。