NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

SpringBootでレイヤをマルチプロジェクトで分割したらメリットが多かったというお話

本記事は  WebアプリWeek   2日目の記事です。
🎣  1日目  ▶▶ 本記事 ▶▶  3日目  🏄

はじめに

はじめまして、石橋章太郎です。
JDK 1.3 の頃から Java を触っています。

昔は Struts がまだ無くて、出てきたときはかなりの感動と衝撃(XML地獄)を受けましたが、それから Spring Framework で登場し、純日本産の Seasar が登場したりと開発者の生産性と品質が向上するフレームワークが色々出てきました。
今は SpringBoot を使っている方が多いのではないかと思っています。 SpringBoot はアノテーションを使ったり、決められた場所に決められたファイルを置けば簡単にアプリケーションが動いてくれます。 それだけに、どのレイヤに何を書くかはチームメンバーや会社の文化によって差があると思っています。 それがのちのちシステム(コード)を運用する上で、管理のやり辛さや品質低下の原因の一因になると考えています。

本記事では、SpringBoot で開発する際にマルチプロジェクト化することで、開発者がレイヤを意識して開発できる仕組み作りの一案をご紹介したいと思います。

前提

ここではある程度 SpringBoot を使ったことのある方を対象としています。
また、O/Rマッパは MyBatis、テンプレートエンジンは Thymeleaf を想定しています。

SpringBoot の基本形について

SpringBoot は Controller や Service、Repository と O/R マッパで基本的には構成しています。
絵にすると以下のようなイメージです。

ただ、実際はファイルをやり取りするためのDTO(Data Transfer Object)や、ユーティリティクラスやヘルパークラスを使うことが多いので、以下のようなイメージになるかと思います。
矢印は各クラスがそれらを参照していることを表しています。

ここで、各クラスの役割を簡単に纏めてみます。 内容は 3.2. ドメイン層の実装 — TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.7.1.SP1.RELEASE documentation を一部参考にしています。

クラス 説明 備考
Controller 入力に関する処理をする。画面描写に必要な処理をする。業務ロジックは全てServiceで行う。 @Controller や @RestController アノテーションを使用。
Service Controllerに対して、業務ロジックを提供する。トランザクション境界。 トランザクション境界はメソッドに @Transactional アノテーションを付与する。
Repository Serviceに対して、データのライフサイクルを制御するための操作を提供する。 Mapperを呼び出してEntityのCRUDをする、外部システムとファイルのやり取りをする等。
Mapper DBアクセスを行う。
Entity テーブルを写像した永続化可能なJavaオブジェクトを提供する。
Form 画面からの入力情報を扱う。
DTO レイヤー間でのデータを扱う。
Util 全メンバ・メソッドを static で定義した共通処理を扱う。 インスタンス化禁止。
Helper 全メンバ・メソッドをインスタンス化して定義した共通処理を扱う。 ヘルパークラスは Helper class - Wikipedia でも定義されている。あくまでヘルパーとしての役割なので、むやみに乱立しないよう注意が必要。

各クラスの役割と参照の関係は上記表と図の通りなのですが、実際は以下の赤線のような参照をしたソースを見ることがあります。

  • Controller からいきなり Repository を呼び出している
  • Service から Mapper を呼び出している
  • Repository から Service を呼び出している
  • Service や Repository で Form を使っている
  • DTO に Form を持っている(もしくは継承している)

こういった意図しない呼び出し方をしてしまうと、さきほど表に記載した各クラスの役割が崩れてしまいます。
また開発者も混乱しますし、保守もしにくくなり、バグの温床にもなります。 それに、ルール通りに書いていたらすんなり理解できることも難しくなります。

事前に開発者に対してルールを周知し、コードレビュー時にレビューアが指摘すれば上記のようなことはある程度防げますが、毎回毎回行うのもなかなかコストがかかりますし、開発者も作ることに集中すると忘れることだってあります。 できれば自然にルールが守れるような構成であったほうが色々負担が減ると思っています。

そこで、マルチプロジェクト化によってレイヤを分割する方法です。

マルチプロジェクト化によるレイヤの分割

マルチプロジェクトについては maven や gradle を使った方法があるため、各サイトを参考にしてください。
私は gradle を使ってマルチプロジェクト化しています。

gradle.monochromeroad.com
(もし具体的な方法とか必要でしたら別記事にしたいと思います)

基本形

基本的な形として、2つのレイヤ(webapp , core)に分割します。

  • webapp
    • プレゼンテーション層とビジネスロジックに集中したプロジェクト
    • アプリケーションの色が付いたもので、他のアプリケーションには流用できないものが多い
    • 静的ページやJS/CSSもこちらに配置する
    • Interceptor や Handler、Filter もこちらで扱う
  • core
    • Mapper や Entity といったDB周りの処理を扱う
    • 外部サービス(AWS、独自API等)とのやり取りを扱う
    • 他のアプリケーションでも流用できる
      • 逆に、アプリケーションのロジックが混じらないよう注意する
メリット

この形にすると以下のようなメリットがあります。

  • core → webapp は呼び出せない
    • core で Form が使われる心配がない
  • webapp 側では表示関連とビジネスロジックに集中できる
  • 煩雑になりがちな DTO や Util , Helper も色分けすることができる
    • webapp の DTO / Util / Helper は Controller / Service でしか使わないものになる
    • 逆にアプリ内全体で扱う DTO / Util / Helper は core に配置する
      • (例) DB から取得した DTO を Service でも使用する場合
      • (例) 日付変換処理、文字列処理 のユーティリティ
  • core 側は Jar 化すれば他のアプリケーションとの相互利用も可能になる
    • 他のアプリケーションで作成した外部接続処理の core を Jar 化して使用する
    • 同じ DB に接続する別のアプリケーションを作成する場合に core の Jar を提供すれば開発不要になる
  • JUnit が書きやすくなる
    • core は DB や外部システムとの In/Out に特化したテストがメインになる
      • テスト用取込データも core 側の test/resources に集約できる
    • webapp は全てモックによるテストになる
      • 時々、DB接続までして確認する Service や Controller の JUnit があるがそれはユニットテストではない…
      • webapp のテストにはDB接続の設定が不要となる
デメリット

逆に以下のようなデメリットがあります。

  • 初期構築が慣れないと大変
  • build.gradle を親プロジェクトと子プロジェクト(webapp, core) に置く必要がある
  • core の application.yml に webapp と同じ定義を一部書く必要がある
    • アプリケーションをただ動かすだけなら webapp の application.yml のみで問題ない
    • core 側で JUnit を実行する際、DB 周りの設定等が必要なため webapp から抜粋して置く必要がある
  • Eclipse 等のツリーが少し見づらくなる(webapp , core それぞれのプロジェクトが表示されるため)
  • gradle でアプリを起動(bootRun)する場合、webapp のプロジェクトも指定しないといけない
    • gradlew [webappのプロジェクト名]:bootRun

発展形

さらに、開発する中で必要となってくるものを追加すると以下の赤文字のようになります。

これらを改めて表に纏めてみます。
基本的な説明は上記表に記載しているので、特色のある部分について記載します。

レイヤクラス説明
webapp Controller 同上
Service 同上
Form 画面からの入力情報を扱う。描写に関する情報は ViewModel に持たせ、Request 本持たせないようにする。
DTO Form から変換しService へ渡したり、webapp 内の Util/Helper 等とのやり取りに使用する。
ViewModel 描写用の POJO。Controller もしくは Util, Helper にて生成し画面表示に使用する。
Config WebMvcConfigurer や @Configuration アノテーションを付与したクラスを扱う。
Enum webapp内でのみ用いるEnum。例えば、画面上で表示や検索項目でよく使われる「ステータス」(未処理、処理中、完了)。
Exception Service や Controller から発生した例外を扱う。アプリケーション全体に関する例外は core に配置した方が扱いやすい。
Util webapp でのみ呼び出される Helper。Controller や Service からの呼び出しがメインになる。例えば、画面表示用の変換処理等。
Helper webapp でのみ呼び出される Helper。Controller や Service からの呼び出しがメインになる。
core Repository 同上
Mapper 同上
Entity 同上
DTO core および webapp で扱うDTO。表示に関する DTO はここには置かないこと。Service ⇔ Repository で扱うDTOはここに配置。
Enum core および webapp で扱う Enum。表示に関する Enum はここには置かないこと。
Exception アプリケーション全体に関するExceptionや、Repository で発生する例外を定義。
Util core および webapp で扱う Util。日付変換処理、文字列処理等。
Helper core および webapp で扱う Helper。

まとめ

今回、マルチプロジェクト化にすることによってレイヤを分けることによるメリットデメリットと、各レイヤのクラスの役割についてご紹介しました。
小さいアプリケーションではあまり効果が見えにくいかもしれませんが、コード量が増えて大きなアプリケーションになるほどこの方法が効いてくると信じています。
皆さんの開発のヒントの1つになれば幸いです。

執筆者石橋章太郎

アプリケーションエンジニア。
Java の開発が好物です。
趣味はカメラ・ドライブ・旅行。