Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Spring Boot利用を前提としたJavaライブラリ開発方法の提案

Spring Boot利用を前提としたJavaライブラリ開発方法の提案

Avatar for Koki Hoshihara

Koki Hoshihara PRO

November 14, 2025
Tweet

Other Decks in Technology

Transcript

  1. @2025 WealthNavi Inc. 星原 宏紀 (Koki Hoshihara)
 ウェルスナビ株式会社 
 サービス基盤グループ ソフトウェアエンジニアリング

    (SWE) チーム
 ⾃⼰紹介 2 ひとこと 最近「UNIXという考え⽅」を読んで感銘を受けました。 ⻑尺のセッションは初めてです。よろしくお願いいたします! ウェルスナビでは 共通ライブラリ開発(⼤規模バッチ / 認証系)、バックエンド開発、 新規システムのパフォーマンスチューニング、新技術導⼊ を推進
  2. © WealthNavi Inc. All Rights Reserved. 5 • ライブラリ化のメリット‧デメリット •

    ライブラリ化を検討する際の機能選定基準 • ライブラリ実装の実例 (unnestの活⽤、JDBCとJPAの併⽤) 本セッションの焦点は、Java/Spring Boot前提の共通ライブラリにおける機能選定です。 ⽣産性と品質を⾼めつつ認知負荷を下げる「⼩さな共通ライブラリ」の機能スコープの 決め⽅を、現場での実践から得た選定基準に基づき共有します。 併せて、ライブラリ化に⾄った機能と実装例を紹介し、ライブラリ化の是⾮を判断する 指針を提供します。具体的には、主に以下の内容をお話しします。 はじめに
  3. © WealthNavi Inc. All Rights Reserved. 6 • Javaでの開発経験がある •

    Spring Boot利⽤を前提としたライブラリ開発に興味がある • 共通ライブラリとして開発するテーマの選定⽅法に興味がある 本セッションのターゲットは、以下のいずれかに当てはまる⽅を想定します はじめに
  4. © WealthNavi Inc. All Rights Reserved. 7 • 既存のOSSライブラリではなく、社内で内製開発されたJavaライブラリ (Spring

    BootなどのJavaのWebフレームワークではない) • 開発された共通ライブラリはいずれもSpring Boot上で動作する • (現時点では)社外にリポジトリを公開する予定はない • 現状のソフトウェアスタックは、Java 21、Spring Boot 3.5系、Gradle 8.13系 ◦ ウェルスナビ全体で進めているJavaおよびSpring Bootのバージョンアップと 平仄を合わせるために上記を選定しました 本セッションにおける「共通ライブラリ」は、以下で定義されます はじめに
  5. © WealthNavi Inc. All Rights Reserved. 9 共通ライブラリが存在しない場合の課題 • 単⼀プロダクトやプロダクトの規模が⼩さい場合

    ◦ 影響範囲が⾒えづらい 横断的関⼼ごと(認証‧DB処理など)が個別に実装されてしまう
  6. © WealthNavi Inc. All Rights Reserved. 10 共通ライブラリが存在しない場合の課題 • 単⼀プロダクトやプロダクトの規模が⼩さい場合

    ◦ 影響範囲が⾒えづらい • プロダクトが増えたり複数プロジェクトが並⾏に動くようになった場合 ◦ プロダクトごとに横断的関⼼ごとが実装されてしまう (=業務ロジックの実装に割く時間が奪われる) ◦ 横断的関⼼ごとの結論は、課題によって異なることもある※1 ◦ 横断的に修正する場合の作業コストが⼤きくなる ◦ 実装ノウハウが分散することにも繋がりかねない 横断的関⼼ごと(認証‧DB処理など)が個別に実装されてしまう ※1: https://speakerdeck.com/irof/glowing-multiple-applications-with-shared-strategy?slide=8
  7. © WealthNavi Inc. All Rights Reserved. 11 共通ライブラリ導⼊のメリット 横断的関⼼ごと(認証‧DB処理など)の実装を集約できる •

    共通ライブラリを使⽤する開発者は、業務ロジックの実装に割ける時間が増える • 共通ライブラリ開発者を中⼼にドキュメンテーションが⾏われるため 横断的関⼼ごとの実装ノウハウを集約しやすい • 有事の際に、業務ロジックに強い開発者と当該ライブラリの実装に詳しい開発者で 問題解決に臨むことができる
  8. © WealthNavi Inc. All Rights Reserved. 12 共通ライブラリ導⼊のメリット(デメリットを⾚字で追記) 横断的関⼼ごと(認証‧DB処理など)の実装を集約できる •

    共通ライブラリを使⽤する開発者は、業務ロジックの実装に割ける時間が増える • 共通ライブラリ開発者を中⼼にドキュメンテーションが⾏われるため 横断的関⼼ごとの実装ノウハウを集約しやすい • 有事の際に、業務ロジックに強い開発者と当該ライブラリの実装に詳しい開発者で 問題解決に臨むことができる • ただし、銀の弾丸ではない ◦ メンテナンスコストがかかる ▪ バージョニング⽅式の検討および実践が必要(当社はSemVerを採⽤) ▪ 社内のMavenリポジトリへのデプロイパイプライン設計‧実装が必要
  9. © WealthNavi Inc. All Rights Reserved. 13 共通ライブラリ導⼊に伴う懸念事項‧対応する考え⽅ 専任のチームが存在することが望ましい 誰が共通ライブラリを保守するのか

    • 共通ライブラリ開発者が不在に なってもメンテナンスできるか • 責務が⼤きなライブラリにより 開発スピードが落ちないか 共通ライブラリが負債にならないか • ウェルスナビの場合はSWEチーム • ただし、開発リソースは有限
  10. © WealthNavi Inc. All Rights Reserved. 14 共通ライブラリ導⼊に伴う懸念事項‧対応する考え⽅ • 共通ライブラリ開発者が不在に

    なってもメンテナンスできるか 誰が共通ライブラリを保守するのか 専任のチームが存在することが望ましい 以下の項⽬をより多く満たす機能を 「⼩さな共通ライブラリ」として実装する 1. ⾞輪の再開発ではなく再利⽤性が⾼い 2. 横断的関⼼ごと 3. プラットフォーム固有の知識が必要 • 責務が⼤きなライブラリにより 開発スピードが落ちないか 共通ライブラリが負債にならないか • ウェルスナビの場合はSWEチーム • ただし、開発リソースは有限
  11. © WealthNavi Inc. All Rights Reserved. 「⼩さな共通ライブラリ」の定義 • 例) 特定のSQL構⽂のSQLビルダー機能

    ◦ 実装: JavaのEntity定義をインプットにリフレクションでSQLを⽣成する 1. 実装する機能のユースケースが明確である • 例) OAuth2.0 の「Resource Server」に必要な機能のみを提供   (認証系の機能は別のライブラリで提供する) など ◦ 実装: JWTのValidatorや全社標準のJWTのプロパティ設定を提供する 2. 全体の構成に対して、単⼀責務のみを実現している
  12. © WealthNavi Inc. All Rights Reserved. 16 ライブラリ化の選定基準を再整理 • ⼀度しか利⽤しない機能はライブラリ化しない

    • 既存のOSSライブラリで提供されている機能は原則開発しない 1. ⾞輪の再開発ではなく再利⽤性が⾼い • 横断的関⼼ごとは設計にコストがかかり難易度が⾼いことが多い 例) ロギング(OTel)、キュー操作(SQS)、キャッシュ操作(Redis) など 2. 横断的関⼼ごと • IDaaSやデータベース固有の実装が必要なもの 例) Auth0のアクセストークンにおけるJWTのプロパティなど※2 3. プラットフォーム固有の知識が必要 ※2: https://auth0.com/blog/jwt-access-tokens-profiles-now-in-ga/
  13. © WealthNavi Inc. All Rights Reserved. 17 ライブラリ化の選定基準を再整理(優先順位付け) • ⼀度しか利⽤しない機能はライブラリ化しない

    • 既存のOSSライブラリで提供されている機能は原則開発しない 1. ⾞輪の再開発ではなく再利⽤性が⾼い • 横断的関⼼ごとは設計にコストがかかり難易度が⾼いことが多い 例) ロギング(OTel)、キュー操作(SQS)、キャッシュ操作(Redis) など 2. 横断的関⼼ごと • IDaaSやデータベース固有の実装が必要なもの 例) Auth0のアクセストークンにおけるJWTのプロパティなど※2 3. プラットフォーム固有の知識が必要 ※2: https://auth0.com/blog/jwt-access-tokens-profiles-now-in-ga/ should must should
  14. © WealthNavi Inc. All Rights Reserved. 18 ライブラリ化の選定基準を再整理(優先順位付け) ※2: https://auth0.com/blog/jwt-access-tokens-profiles-now-in-ga/

    • ⼀度しか利⽤しない機能はライブラリ化しない • 既存のOSSライブラリで提供されている機能は原則開発しない 1. ⾞輪の再開発ではなく再利⽤性が⾼い • 横断的関⼼ごとは設計にコストがかかり難易度が⾼いことが多い 例) ロギング(OTel)、キュー操作(SQS)、キャッシュ操作(Redis) など 2. 横断的関⼼ごと • IDaaSやデータベース固有の実装が必要なもの 例) Auth0のアクセストークンにおけるJWTのプロパティなど※2 3. プラットフォーム固有の知識が必要 3つ⽬の実装は様々な意⾒がありますが より専⾨的な知識が要求されるため以下 2点を満たす場合に実装することを推奨 「当該プラットフォームの有識者が揃っ ている」 「プラットフォームに依存することのデ メリットをメリットが上回る」 should must should
  15. © WealthNavi Inc. All Rights Reserved. 20 プロジェクト概要とunnestの採⽤に⾄った経緯 • 外部キー制約をサポートし、パーティション導⼊できる点からPostgreSQLを採⽤

    • ⼤規模バッチであることから、バルクインサートを実装する必要があった • ウェルスナビではMySQLが中⼼であり、PostgreSQLの開発ノウハウは少なかった 新規プロジェクトにて、 Java(Spring Boot)‧PostgreSQLの組み合わせで⼤規模バッチシステムを開発 • 調査を踏まえチーム内レビューで以下の点を評価し、unnestを使ったINSERT⽅式を採⽤ ◦ SQLのパラメータ数の上限を実質的に無視できる ◦ パラメータ数の考慮が不要でありながら⼗分に⾼速※3 PostgreSQLバルクインサートの実装⽅法を調査し、SQLレベルでの性能を検証した ※3: https://www.tigerdata.com/blog/boosting-postgres-insert-performance
  16. © WealthNavi Inc. All Rights Reserved. 21 従来のVALUES句を使ったINSERTとunnest(配列展開)を使ったINSERTの違い INSERTにおける両者の違いを表に整理する VALUES句を使ったINSERT

    unnestを使ったINSERT 構造/ INSERT⽂のサンプル パラメータ数/ SQLの⻑さ ⾏×列/⻑くなりやすい 列だけ/短く保てる SQLの実⾏負荷 ⾏数に⽐例して増加 列数に概ね⽐例 VALUES (:a1, :b1, :c1), (:a2, :b2, :c2), (:a3, :b3, :c3) INSERT INTO t(c1,c2,c3) VALUES (:a1,:b1,:c1), (:a2,:b2,:c2), (:a3,:b3,:c3) :col1 -> [ a1, a2, a3 ] :col2 -> [ b1, b2, b3 ] :col3 -> [ c1, c2, c3 ] INSERT INTO t(c1,c2,c3) SELECT * FROM unnest(:col1,:col2,:col3) AS t(c1, c2, c3)
  17. © WealthNavi Inc. All Rights Reserved. 参考: 複数⾏INSERTとunnestを⽤いたINSERTの性能⽐較 PostgreSQL は配列を⾏に展開する関数「unnest」をサポートする

    そこで、INSERT時に複数⾏のVALUES句を使⽤するケースとunnestを使⽤したケースの 性能を⽐較したところPlanning Time、Execution Timeともにunnestの⽅が優れていた ※M3 MacBookPro上でPostgreSQL17(コンテナ)で検証した結果を添付 22
  18. © WealthNavi Inc. All Rights Reserved. 23 1. ⾞輪の再開発ではなく再利⽤性が⾼い 2.

    横断的関⼼ごと 3. プラットフォーム固有の知識が必要 (再掲)ライブラリ化検討時の重要項⽬ 1. (複数バッチで使⽤されるため) unnest⽤のSQLビルダーを提供してほしい 2. (処理次第でJPAのメソッドを使いたいため) Spring Data JPAのJpaRepositoryと Spring JDBCのRepositoryの共存 要求仕様 要求仕様に対するライブラリ化の是⾮を検討 ライブラリ化選定基準の1~3をすべて満たすため、ライブラリ化が決定
  19. © WealthNavi Inc. All Rights Reserved. 24 1. (複数バッチで使⽤されるため) unnest⽤のSQLビルダーを提供してほしい

    要求仕様 要求仕様 1 に対する実装イメージ(1/4) 実装 • ライブラリ側の実装全量はキャプチャの7クラスのみ
  20. © WealthNavi Inc. All Rights Reserved. 25 要求仕様 1 に対する実装イメージ(2/4)

    ファイル名 概要説明 AbstractBulkEntity • Entityのリストをバルクインサート用の配列に変換する基底クラス • Javaのフィールドを指定された型の配列に変換するユーティリティを提供 • アプリケーション側で AbstractBulkEntityを継承するクラスを実装する BulkInsertExecutor • JdbcClientを用いてunnest構文を使ったバルクインサート SQLを作成・実行す る BulkColumn • データベースのカラム名と PostgreSQLの型を保持するアノテーション バルクインサート用 の独自例外クラス群 • バルクインサート用の独自例外を定義する • 例外ハンドリングはライブラリ側で実装する ライブラリで実装するクラスの⼀覧
  21. © WealthNavi Inc. All Rights Reserved. 26 要求仕様 1 に対する実装イメージ(3/4)

    役割 概要説明 Javaの型を DBの型に変換する ユーティリティ (継承用) • Entityのリストをバルクインサート用の配列に変換する基底クラス • Javaのフィールドを指定された型の配列に変換するユーティリティを提供 • アプリケーション側で AbstractBulkEntityを継承するクラスを実装する INSERT文の 組み立て・実行 • JdbcClientを用いてunnest構文を使ったバルクインサート SQLを作成・実行す る 次スライドで以下の役割について実装イメージを説明
  22. © WealthNavi Inc. All Rights Reserved. 要求仕様 1 に対する実装イメージ(4/4) unnestを⽤いたバルクインサートの実⾏イメージ

    1. ‧処理のエントリーポイント 2. ‧変換ユーティリティを継承したクラスを引数に取り    Javaフィールドをテーブルの各カラムの型にマッピング 3. ‧INSERTで使⽤する以下の形式のSQLを組み⽴てる    INSERT INTO ... SELECT * FROM unnest(...) as t(...) 4. ‧INSERTを実⾏しデータベースに永続化   ‧処理結果をServiceに返却 Service (tx境界) Repository Handler 27
  23. © WealthNavi Inc. All Rights Reserved. 28 要求仕様 2 に対する実装イメージ

    実装イメージ ※4:https://spring.pleiades.io/spring-data/jpa/reference/repositories/custom-implementations.html BookRepository (interface) extends JpaRepository, BookJdbcClientRepository BookJdbcClientRepositoryImpl (class) BookJdbcClientRepository (interface) JpaRepository (interface) Spring Dataが ランタイムに 合成※4 1. unnest⽤のSQLビルダーい 2. Spring Data JPAのJpaRepositoryと Spring JDBCのRepositoryの共存 要求仕様
  24. © WealthNavi Inc. All Rights Reserved. 29 Spring JDBCのみを使⽤する場合 //

    package, importは省略 @Repository public class BookJdbcRepository { private final JdbcClient jdbc; public BookJdbcRepository(JdbcClient jdbc) { this.jdbc = jdbc; } //処理省略 String sql = """  INSERT INTO book (title, author) SELECT * FROM unnest(:title, :author) AS t(title, author) """; return jdbc.sql(sql) .param("title", title) .param("author", author) .update(); } } 本来、以下のようにSpring JDBCに依存したRepositoryのみを使⽤することになる
  25. © WealthNavi Inc. All Rights Reserved. 30 Spring JDBCとSpring Data

    JPAの併⽤(1/3) package com.example.book.repository; import com.example.book.domain.Book; import org.springframework.data.jpa.repository.JpaRepository; public interface BookRepository extends JpaRepository<Book, Long>, BookJdbcRepository { // JPAのメソッドもここに定義可 } 以下のように実装することでSpring JDBCとSpring Data JPAを併⽤可能 JPA側の本体のRepository
  26. © WealthNavi Inc. All Rights Reserved. 31 Spring JDBCとSpring Data

    JPAの併⽤(2/3) package com.example.book.repository; import com.example.book.domain.Book; import java.util.List; public interface BookJdbcRepository { int bulkInsert(List<Book> books); } カスタム断⽚のInterface(JDBC側) 以下のように実装することでSpring JDBCとSpring Data JPAを併⽤可能
  27. © WealthNavi Inc. All Rights Reserved. 32 Spring JDBCとSpring Data

    JPAの併⽤(3/3) カスタム断⽚のInterface(JDBC側)のImplクラス(必ず <Interface名>Impl) にする必要あり 以下のように実装することでSpring JDBCとSpring Data JPAを併⽤可能 // package, importは省略 @Repository public class BookJdbcRepositoryImpl implements BookJdbcRepository { private final JdbcClient jdbc; public BookJdbcRepositoryImpl(JdbcClient jdbc) { this.jdbc = jdbc; } @Override public int bulkInsert(List<Book> books) { String[] title = books.stream().map(Book::getTitle).toArray(String[]::new); String[] author = books.stream().map(Book::getAuthor).toArray(String[]::new); // 以降は P.28と同様のため省略 } }
  28. © WealthNavi Inc. All Rights Reserved. 34 共通ライブラリのテスト⽅針 • ユニットテスト

    ◦ 条件網羅、分岐網羅は100%を⽬指す ▪ 難しければ、クラス/メソッド分割でよりテストしやすいコードに修正 • 可視化 ◦ JaCoCoカバレッジをコード品質管理ツール(当社ではQodana※5 )に連携し可視化 • 統合テスト ◦ サンプルアプリケーションを⽤いたテストを⾏う ◦ 公開API、設定、独⾃例外のテストを⾏う ライブラリではユニットテスト、統合テストともに実施する ※5:https://www.jetbrains.com/ja-jp/qodana/
  29. © WealthNavi Inc. All Rights Reserved. 35 共通ライブラリを取り込むアプリケーションのテスト⽅針 アプリケーションでは必要最⼩限の統合テストのみを⾏う •

    ユニットテスト ◦ アプリケーション⾃⾝のユニットテストは⾏うが、ライブラリ内部に踏み込むテ ストはしない(⼆重化を避ける) • 統合テスト ◦ アプリケーションで使⽤するライブラリの公開APIのテストや設定値のバインド、 Bean起動などをテストする • Tips ◦ 初期はUtilクラスとして実装し、ライブラリ化後に Gradle/Maven 取り込みに切り 替えると、取り込み時のBean関連の問題切り分けが容易
  30. © WealthNavi Inc. All Rights Reserved. 36 共通ライブラリのドキュメンテーション • クラスレベルのJavadocを必ず書く

    • メソッドは概要と(返却する例外が明確なときは@throws)、 @param、@returnを記載 ◦ レビュアーの指摘負荷を減らすために上記の内容をcopilot-instructions※6 に記載 Javadoc • GitHubではMarkdownファイルをWiki化できるため、この仕組みを利⽤ GitHubのWiki • 全社横断で閲覧可能なNotionページにDesign Docとして配置 • 実装する/しないの整理や、どの機能をどのVersionでリリースするかを記載 Design Doc ※6:https://docs.github.com/ja/copilot/how-tos/configure-custom-instructions/add-repository-instructions#creating-custom-instructions
  31. © WealthNavi Inc. All Rights Reserved. 38 まとめ 1. ターゲットおよび共通ライブラリの定義

    2. 共通ライブラリのメリット/デメリット そしてライブラリ化の選定基準 3. PostgreSQLのunnestを活⽤したバルクインサートの ライブラリ化までの過程 4. 共通ライブラリにおけるテストとドキュメンテーション 本セッションでは、各章で以下の内容をお話してきました。 2章を厚く作成したのは「作成したライブラリは誰かが保守する必要があること」とその リスクに対して「⼩さな共通ライブラリ」を作成するメリットを伝えたかったためです。 今回のセッションが開発者の皆さまの参考になれば幸いです。 それでは、素敵なライブラリ開発ライフを!
  32. © WealthNavi Inc. All Rights Reserved. • 本資料は、断定的判断を提供するものではなく、情報を提供することのみを目的としており、いかなる種類の商品も勧誘 するものではありません。最終的な決定は、お客様自身で判断するものとし、当社はこれに一切関与せず、また、一切の 責任を負いません。

    • 本資料には将来の出来事に関する予想が含まれている場合がありますが、それらは予想であり、また、本資料の内容の 正確性、信頼性、完全性、適時性等を一切保証するものではありません。本資料に基づいて被ったいかなる損害について も、当社は一切の責任を負いません。また、当社は、新しい情報や将来の出来事その他の情報について、更新又は訂正 する義務を負いません。 • 本資料を利用することによりお客様に生じた直接的損害、間接的損害、派生的損害その他いかなる損害についても、当社 は一切の責任を負いません。 商号等:ウェルスナビ株式会社 金融商品取引業者 関東財務局長(金商) 第2884号 加入協会:日本証券業協会 一般社団法人日本投資顧問業協会 重要な注意事項