📄

Spring WebMVC/WebFlux で実現する省エネなスキーマ駆動開発

に公開

はじめに

“スキーマ駆動開発 (Schema‑Driven Development)” という概念があります。APIのドキュメントをOpenAPIのスキーマとしてまず定義し、そのスキーマからサーバーコードとクライアントコードを自動生成することでドキュメントと実装の乖離を防ぎ、開発サイクルの高速化と品質を担保する手法です。

一方で、OpenAPIのスキーマを人間が手で書くのはやや体験が悪いです。
その解決策としてtypespecなどもありますが、今回は紹介しません。

Spring WebMVC/WebFluxにおいては、springdoc-openapiを使うことで、Springのコントローラー実装からOpenAPIスキーマを自動生成することができます。OpenAPIスキーマがあればHTMLドキュメントやクライアントコードを自動生成することができるため、これだけでも一定の価値があります。

しかし、このケースだとコントローラー実装からOpenAPIスキーマを生成しているので、スキーマ駆動開発と呼ぶにはふさわしくないでしょう。

実はSpring 5.1からコントローラーに対してインターフェースを使うことができるようになったことをご存知でしょうか?このインターフェースを使うことで、スキーマ駆動開発を比較的省エネで実現することができるようになっています。

従来のコントローラー定義をインターフェースに分けて書く

例えば、こういった感じです。*Exchangeという人によっては見慣れないアノテーションを使っていますが、これを使っている理由は後述します。従来通りに*Mappingを使っても問題ありません。

@HttpExchange("/api/v1/blogs")
interface BlogApi {
    @GetExchange("/")
    @Operation(
        summary = "ブログの一覧を取得",
        description = "blah blah blah"
    )
    fun list(): List<Blog>

    @PostExchange("/")
    @Operation(
        summary = "ブログを作成する",
        description = "blah blah blah"
    )
    fun create(@RequestBody request: BlogCreateRequest): Blog

    @GetExchange("/{id}")
    @Operation(
        summary = "指定したIDのブログを取得する",
        description = "blah blah blah"
    )
    fun get(@PathVariable id: Int): Blog

    @PutExchange("/{id}")
    @Operation(
        summary = "指定したIDのブログを更新する",
        description = "blah blah blah"
    )
    fun update(@PathVariable id: Int, @RequestBody request: BlogUpdateRequest): Blog

    @DeleteExchange("/{id}")
    @Operation(
        summary = "指定したIDのブログを削除する",
        description = "blah blah blah"
    )
    fun delete(@PathVariable id: Int)
}

data class Blog(val id: Int, val title: String, val body: String)

data class BlogCreateRequest(val title: String, val body: String)

data class BlogUpdateRequest(val title: String, val body: String)

これに対して、次のように実装を用意します。

@RestController
class BlogApiController : BlogApi {
    // 今回は実装の中身には関心がないので省略
}

インターフェースを使うことで、次のメリットが生まれます。

  • インターフェースがAPIスキーマとなる
    • これをもとにOpenAPIスキーマやHTMLドキュメントをspringdoc-openapiを使って生成することができる
  • インターフェースだけ先に用意&レビューすることができるため、きちんとスキーマ駆動開発をすることができる
  • 従来の方法だと、コントローラーに大量のswaggerアノテーションと実装が混在するため、いまいち内容を把握しにくかった
    • 上の例だとswaggerアノテーションの量が少ないですが、真面目にやるとここの量がけっこう増えてきます。結果、コントローラーがゴチャゴチャしてきます
    • レビュワーがレビューもしやすい

新しいツールなどが一切不要な点も、省エネで始めやすくていいのかなと思います。仮にやめたくなったときも簡単にやめることができます。
(springdoc-openapiは必要ですが、これの導入はそこまで大変ではないので...)

冒頭で少しだけ触れたtypespecはOpenAPIスキーマをTypeScript風味なコードを使って定義するものでしたが、インターフェースを使うことでこれと似たような開発体験に近づきます。(typespecはJSON SchemaやProtobufの定義にも使える点で、より高機能ではあるんですが...)

springdoc-openapiを使ってOpenAPIスキーマを生成する

これについては従来のコントローラーを使った方法となにも変更点がないので、説明は割愛します。
(インターネット上にも十分解説記事もありますし...)

開発フロー

つまり、こういった開発フローに落とし込むことができます。

flow

応用: Spring HTTP Interface Clientと一緒に使う

Spring 6からHTTP Interface Clientという機能が登場しています。

登場当初はただのRetrofitのパチモンだと思っていた(ごめんなさい...)のですが、実はコントローラーインターフェースとしても使うことができます。
先程のコントローラーインターフェースの例で*Exchangeという人によっては見慣れないアノテーションを使っていますが、このアノテーションはこのHTTP Interfaceのものです。

つまり、もしもクライアント側もSpringを使っている場合は、コントローラーインターフェースとそれに関連するモデルクラスをライブラリとして切り出しておくだけでAPIクライアントを簡単に作ることができます。

val webClient = WebClient.builder()
    .baseUrl("http://localhost:8080/")
    .build()
val proxyFactory = HttpServiceProxyFactory.builderFor(WebClientAdapter.create(webClient)).build()
// 簡単にAPIクライアントを作れる
val blogApi = proxyFactory.createClient(BlogApi::class.java)

マイクロサービスとしてSpringを多用しているような環境なら、有用だと思われます。

ただし、サーバー側のインタフェースを共有する都合上、片方だけReactive(Mono/Flux)にすることができないのは難点です。

まとめ

本記事では、Spring WebMVC/WebFluxにおけるスキーマ駆動開発を実現する一つのアプローチとして、コントローラーインターフェースの活用について紹介しました。

従来は、OpenAPIスキーマをSpringのコントローラー実装から生成していましたが、これではスキーマ駆動開発とは言えませんでした。Spring 5.1以降で使えるコントローラーインターフェースを活用することで、先にスキーマを定義し、それをベースに実装やクライアント生成を行うという開発スタイルが可能になります。

また、このインターフェースはSpring 6で導入されたHTTP Interface Clientとも組み合わせることができる点も良いです。

スキーマ駆動開発をしたいならgRPCを使ったほうが良い...という意見もありますが、今回紹介する方法を使うことで、それに近い開発体験に近づけることはできるのかなと思います。

Discussion