サーブレット&JSPの基本|Springのモデルとサービスの基本

データを入れるクラスはシンプルに、処理を担当するクラスはサービスとして整理する。LombokとDIを使って、Springらしいモデル開発の基本を身に付けよう

Spring MVCでは、WebアプリケーションをModel、View、Controllerの役割に分けて開発します。

前回までに、Springのコントローラはリクエストを受け取り、必要な処理を呼び出し、ビュー名を返す役割を持つことを学習しました。

では、コントローラから呼び出される処理や、処理で扱うデータはどこに書けばよいのでしょうか。

その中心になるのがモデルです。

これまでServletとJSPで学んできたWebアプリケーションでも、Userクラス、LoginLogicクラス、DAOクラスなどをmodelパッケージに分けて作成してきました。

Springでも、考え方は大きく変わりません。

データを格納するクラスは、通常のJavaクラスとして作成します。

アプリケーションの主な処理を担当するクラスも、通常のJavaクラスとして作成します。

ただし、Springでは処理を担当するクラスをサービスと呼び、@Serviceアノテーションを付けます。

また、コントローラからサービスを利用するときは、newでインスタンスを作るのではなく、Springが用意する依存性の注入という仕組みを使います。

最初は少し不思議に感じるかもしれません。

しかし、Springではこの仕組みによって、クラス同士のつながりを整理し、コードを読みやすくし、テストしやすい構成を作れるようになっています。

この記事では、spring_studyプロジェクトを使い、データを格納するクラスの作り方、Lombokによる定型コードの削減、サービスクラスの作成、コントローラからサービスを利用する方法、そしてコンストラクタインジェクションの基本を順番に学習します。

Springにおけるモデルとは

Spring MVCのモデルは、アプリケーションの処理や、その処理で扱うデータを担当する部分です。

ServletとJSPで学んだMVCモデルでも、モデルは処理やデータを担当していました。

Springでも、この考え方は同じです。

モデルに含まれる主なクラス

種類役割
データを格納するクラス画面や処理で扱う値をまとめる商品情報、会員情報、入力フォームの情報
サービスクラスアプリケーションの主な処理を担当する計算処理、ログイン判定、一覧取得処理
Entityデータベースのテーブル1件分を表す商品レコード、会員レコード
DTOクラス間で受け渡すデータをまとめる画面表示用データ、検索条件
DAOやRepositoryデータベースアクセスを担当する商品検索、会員登録、注文保存

この記事では、まずデータを格納するクラスと、サービスクラスに注目します。

データを格納するクラスは、フィールド、コンストラクタ、getter、setterを持つ通常のJavaクラスとして作成します。

サービスクラスは、アプリケーションの中心となる処理を担当し、@Serviceを付けてSpringに管理してもらいます。

データを格納するクラスの考え方

データを格納するクラスは、複数の値をひとまとまりにして扱うためのクラスです。

たとえば、商品名と価格を別々の変数として扱うよりも、1つの商品情報としてまとめたほうが扱いやすくなります。

データを格納するクラスで用意するもの

要素内容
フィールド保存したいデータを表す変数
引数を持たないコンストラクタSpringやライブラリがインスタンスを作成しやすくするために用意する
引数を持つコンストラクタ初期値をまとめて設定したいときに使う
getterフィールドの値を取得する
setterフィールドの値を変更する

このようなクラスは、Springでも通常のJavaクラスとして作成します。

特別な継承やアノテーションが必ず必要になるわけではありません。

まずは、これまで学んできたJavaBeansの考え方と同じように作れば大丈夫です。

モデルクラスの作成

モデルを作成するSpringプロジェクトを選択して、右クリック→「新規」→「クラス」を選択します。

モデルクラスが所属するパッケージとクラス名を指定して、「完了」をクリックします。

パッケージは「com.example.demo.model」、クラス名に「CatalogItem」を指定します。

src/main/java」内にクラスが作成され、エディタには作成したクラスの内容が表示されます。
サービスを作成する場合は、「@Service」アノテーションを付与します。

「org.springframework.stereotype.Service」をインポートします。

以下のコードを追加します。

import org.springframework.stereotype.Service;

データをまとめるクラスの例

ファイル名: CatalogItem.java

package com.example.demo.model;

public class CatalogItem {
    private String itemName;
    private int price;

    // 引数を持たないコンストラクタ
    public CatalogItem() {
    }

    // すべてのフィールドを引数にもつコンストラクタ
    public CatalogItem(String itemName, int price) {
        this.itemName = itemName;
        this.price = price;
    }

    // getterとsetter
    public String getItemName() {
        return itemName;
    }

    public void setItemName(String itemName) {
        this.itemName = itemName;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}

CatalogItem.javaは、com.example.demo.modelパッケージに作成します。

このクラスは、カタログに表示する商品情報をまとめるためのクラスです。

itemNameフィールドには商品名を保存します。

priceフィールドには価格を保存します。

引数を持たないコンストラクタは、Springや各種ライブラリがインスタンスを作成するときに使いやすい形です。

すべてのフィールドを引数にもつコンストラクタは、商品名と価格をまとめて設定したいときに使います。

getterとsetterを用意しておくことで、外部のクラスからフィールドの値を取得したり、変更したりできます。

図1:データを格納するクラスは値をまとめる入れ物

この図から分かること

データを格納するクラスは、複数の値をひとまとまりにするための入れ物です。

CatalogItemクラスであれば、商品名と価格を1つの商品データとして扱えます。

このようなクラスを用意しておくと、コントローラやサービスの間でデータを受け渡しやすくなります。

Springでも、データを格納するクラスは通常のJavaクラスとして作成します。

ボイラープレートコードとは

データを格納するクラスでは、フィールド、コンストラクタ、getter、setterをよく書きます。

小さなクラスであれば、それほど大変ではありません。

しかし、プロジェクトが大きくなると、同じような形のクラスがたくさん増えていきます。

会員情報を表すクラス。

商品情報を表すクラス。

注文情報を表すクラス。

検索条件を表すクラス。

フォーム入力値を表すクラス。

このようなクラスが増えるたびに、同じようなコンストラクタやgetter、setterを何度も書くことになります。

このように、毎回ほぼ同じ形で書く必要がある定型的なコードを、ボイラープレートコードと呼びます。

ボイラープレートコードが増えると起こりやすい問題

問題内容
コードが長くなる本当に重要な処理が見えにくくなる
修正漏れが起きやすいフィールドを追加したのにgetterやsetterを追加し忘れる
コピーミスが起きる別のフィールド名のまま貼り付けてしまう
可読性が下がるクラスの本質が分かりにくくなる
作成に時間がかかる同じようなコードを繰り返し書く必要がある

データを格納するクラスでは、本当に大切なのは、どのフィールドを持っているかです。

しかし、getterやsetterがたくさん並ぶと、クラス全体が長くなり、フィールドの意味が見えにくくなることがあります。

そこで役立つのがLombokです。

Lombokとは

Lombokは、Javaの定型的なコードを自動生成してくれるライブラリです。

getterやsetter、コンストラクタなどを自分で書かなくても、アノテーションを付けるだけでLombokが自動的に用意してくれます。

LombokはSpring専用の機能ではありません。

Javaプロジェクト全般で利用できる一般的なライブラリです。

ただし、Springを使った開発ではとてもよく利用されます。

Spring Initializrでプロジェクトを作成するときにLombokを選択しておくと、最初から利用できる状態にできます。

Lombokを使うメリット

メリット内容
コードが短くなるgetter、setter、コンストラクタの記述を減らせる
可読性が上がるクラスが持つフィールドを確認しやすくなる
コピーミスを減らせる手作業でgetterやsetterを複製する必要が減る
修正しやすくなるフィールド追加時の定型コード作成を減らせる
開発効率が上がるモデルクラスを素早く作成できる

Lombokを使うと、データを格納するクラスをかなりシンプルに書けます。

Lombokを使用したクラスの例

ファイル名: CatalogItem.java

package com.example.demo.model;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class CatalogItem {
    private String itemName;
    private int price;
}

このCatalogItem.javaは、先ほど手書きしたCatalogItemクラスと同じように使えます。

@Getterを付けると、すべてのフィールドに対するgetterが自動生成されます。

@Setterを付けると、すべてのフィールドに対するsetterが自動生成されます。

@NoArgsConstructorを付けると、引数を持たないコンストラクタが自動生成されます。

@AllArgsConstructorを付けると、すべてのフィールドを引数にもつコンストラクタが自動生成されます。

つまり、itemNameとpriceのフィールドを書くだけで、必要な定型的なメソッドやコンストラクタをLombokに任せられます。

Lombokが提供する代表的なアノテーション

Lombokには、さまざまなアノテーションがあります。

ここでは、モデルクラスでよく使うものを確認します。

Lombokの代表的なアノテーション

アノテーション自動生成されるメソッド・コンストラクタ
@Getterすべてのフィールドのgetter
@Setterすべてのフィールドのsetter
@Dataすべてのフィールドのgetterとsetter、およびtoString、equals、hashCode
@NoArgsConstructor引数を持たないコンストラクタ
@AllArgsConstructorすべてのフィールドを引数にもつコンストラクタ
@RequiredArgsConstructorfinalが付いているフィールドを引数にもつコンストラクタ

@Getterと@Setterを両方付ける代わりに、@Dataを使うこともできます。

@Dataを付けると、getterとsetterだけでなく、toString、equals、hashCodeも自動生成されます。

ただし、@Dataは多くのメソッドをまとめて生成するため、どのメソッドが必要なのかを考えながら使うことが大切です。

単純に値を入れて受け渡すクラスであれば便利ですが、クラスの用途によっては@Getterや@Setterを個別に付けたほうが分かりやすい場合もあります。

Lombokを使うときの注意点

Lombokのアノテーションを使うには、lombokパッケージのクラスをimportする必要があります。

たとえば、@Getterを使うにはlombok.Getterをimportします。

@AllArgsConstructorを使うにはlombok.AllArgsConstructorをimportします。

また、開発環境によっては、Lombokを利用するための設定が必要になることがあります。

Spring Initializrでプロジェクトを作成するときにLombokを選んでいれば、依存関係は準備されます。

ただし、エディタ上で補完やエラー表示がうまく動かない場合は、Eclipse側のLombok設定も確認しましょう。

Lombok利用時に確認すること

確認項目内容
importlombok.Getterなど必要なクラスをimportしているか
依存関係SpringプロジェクトでLombokを利用できる状態になっているか
アノテーションクラスに必要なLombokアノテーションを付けているか
用途@Dataでよいか、@Getterと@Setterを分けるかを考える
動作確認getterやsetterを利用するコードで正しく動くか確認する

Lombokは便利ですが、何が自動生成されているのかを意識して使うことが大切です。

図2:Lombokでボイラープレートコードを減らす

この図から分かること

データを格納するクラスでは、getter、setter、コンストラクタなどの定型的なコードが多くなりがちです。

Lombokを使うと、@Getterや@Setterなどのアノテーションを付けるだけで、これらの定型コードを自動生成できます。

その結果、クラスにどのフィールドがあるのかが分かりやすくなります。

ボイラープレートコードを減らすことで、コードの見通しがよくなり、修正漏れやコピーミスも減らしやすくなります。

サービスとは

Springでは、アプリケーションの主な処理を担当するクラスをサービスと呼びます。

ServletとJSPの学習で作成してきたLogicクラスに近いものです。

たとえば、ログイン判定を行うLoginLogic、投稿一覧を取得するGetMessageListLogic、投稿を登録するPostMessageLogicのようなクラスは、Springではサービスとして考えます。

サービスの役割

役割内容
主な処理を担当するアプリケーションの中心となる処理を書く
コントローラから呼び出されるリクエストを受けたコントローラがサービスに処理を依頼する
データを加工する入力値を使って計算や判定を行う
DAOやRepositoryを呼び出す必要に応じてデータベースアクセスを依頼する
結果を返す処理結果をコントローラへ返す

サービスクラスは、通常のJavaクラスとして作成します。

ただし、Springにサービスとして認識してもらうために、@Serviceアノテーションを付けます。

@Serviceを使うには、org.springframework.stereotype.Serviceをimportします。

サービスクラスの名前

Springでは、サービスクラスの名前の末尾にServiceを付けるのが一般的です。

たとえば、ポイント計算を担当するならPointService、注文合計を計算するならOrderTotalService、ログイン処理を担当するならLoginServiceのようにします。

サービスクラス名の例

処理サービスクラス名の例
ポイントを計算するPointService
合計金額を計算するTotalPriceService
ログインを判定するLoginService
商品一覧を取得するProductListService
注文を登録するOrderRegisterService

名前の末尾にServiceを付けることで、このクラスが処理を担当するモデルであることが分かりやすくなります。

サービスの例

ファイル名: PointService.java

package com.example.demo.model;

import org.springframework.stereotype.Service;

@Service
public class PointService {

    public int execute(int basePoint, int campaignPoint) {
        return basePoint + campaignPoint;
    }
}

PointService.javaは、com.example.demo.modelパッケージに作成します。

このクラスは、基本ポイントとキャンペーンポイントを足し合わせるシンプルなサービスです。

@Serviceを付けることで、Springはこのクラスをサービスとして認識します。

executeメソッドは、basePointとcampaignPointを受け取り、その合計を返します。

このように、サービスにはアプリケーションの主な処理を書きます。

今回は足し算だけの簡単な例ですが、実際のWebアプリケーションでは、ログイン判定、注文金額の計算、商品一覧の取得、入力チェックなどをサービスに書いていきます。

コントローラからサービスを利用する

コントローラは、リクエストを受け取ったあと、必要に応じてサービスに処理を依頼します。

通常のJavaであれば、サービスを使う前にnewでインスタンスを作成します。

しかし、Springではコントローラの中でnewを使ってサービスを作るのは一般的ではありません。

Springでは、依存性の注入という仕組みを使って、必要なサービスのインスタンスをSpringに用意してもらいます。

newを使わない理由

理由内容
Springが管理できるようにするためサービスの生成や利用をSpringに任せられる
テストしやすくするため必要に応じて別のサービスやテスト用部品に差し替えやすい
結合を弱くするためコントローラが具体的な生成方法を知りすぎない
保守しやすくするためクラス同士のつながりを整理しやすい

コントローラがサービスを利用する場合は、コンストラクタインジェクションを使うのが基本です。

依存性の注入とは

依存性とは、あるクラスが別のクラスを必要としている関係のことです。

たとえば、コントローラがサービスを呼び出して処理を行う場合、コントローラはサービスに依存していると言えます。

依存性の注入は、その必要なオブジェクトを自分でnewして作るのではなく、外部から渡してもらう仕組みです。

Springでは、この外部から渡してもらう役割をSpring自身が担当します。

依存性の注入のイメージ

考え方内容
依存している状態コントローラがサービスを使って動く
依存先サービスのインスタンス
注入必要なサービスを外部から渡してもらう
Springの役割サービスを作成し、コントローラへ渡す

この仕組みにより、コントローラはサービスの作り方を意識しなくてよくなります。

コントローラは、渡されたサービスを使って処理を進めます。

コンストラクタインジェクションを使う手順

コンストラクタインジェクションは、コンストラクタを使って必要なオブジェクトを受け取る方法です。

Springでは、現在よく使われる推奨されるDIの方法です。

コンストラクタインジェクションの手順

順番内容
1コントローラに@Controllerを付ける
2コントローラにサービスを代入するフィールドを定義する
3サービスのフィールドにfinalを付ける
4コントローラに@RequiredArgsConstructorを付ける
5サービスに@Serviceを付ける

@RequiredArgsConstructorは、Lombokが提供するアノテーションです。

finalが付いているフィールドを引数にもつコンストラクタを自動生成します。

Springは、そのコンストラクタを使って、サービスのインスタンスをコントローラへ渡します。

サービスを利用するコントローラの例

ファイル名: PointController.java

package com.example.demo.controller;

import jakarta.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.example.demo.model.PointService;

import lombok.RequiredArgsConstructor;

@Controller
@RequiredArgsConstructor
public class PointController {

    // サービスが注入されるフィールド
    private final PointService pointService;

    @PostMapping("/point/add")
    public String calculatePoint(
            @RequestParam int basePoint,
            @RequestParam int campaignPoint,
            HttpServletRequest request) {

        // サービスに処理を依頼
        int totalPoint = this.pointService.execute(
                basePoint, campaignPoint);

        // 処理結果をリクエストスコープに保存
        request.setAttribute("totalPoint", totalPoint);

        // 結果を表示するビュー名を返す
        return "pointResult";
    }
}

PointController.javaは、com.example.demo.controllerパッケージに作成します。

このコントローラは、/point/addへのPOSTリクエストを受け取ります。

basePointとcampaignPointというリクエストパラメータを@RequestParamで受け取り、PointServiceのexecuteメソッドに渡します。

PointServiceのexecuteメソッドは、2つのポイントを足し合わせて返します。

その結果をtotalPointとして受け取り、request.setAttributeでリクエストスコープに保存します。

最後にpointResultというビュー名を返します。

このように、コントローラはリクエストを受け取り、サービスに処理を依頼し、結果をビューへ渡す準備をします。

サービスのインスタンスはnewしていません。

@RequiredArgsConstructorとfinalフィールドにより、Springが自動的にPointServiceインスタンスを代入してくれます。

図3:コンストラクタインジェクションでサービスが注入される流れ

この図から分かること

コンストラクタインジェクションでは、Springがサービスのインスタンスを用意します。

コントローラを作成するときに、そのサービスインスタンスをコンストラクタへ渡します。

@RequiredArgsConstructorを使うと、finalが付いたフィールドを引数にもつコンストラクタをLombokが自動生成してくれます。

そのため、コントローラのフィールドにサービスが自動的に代入されます。

コントローラはサービスをnewせず、Springから渡されたサービスを使って処理を行います。

@RequiredArgsConstructorを使わない場合

@RequiredArgsConstructorを使うと、コンストラクタを自分で書かずに済みます。

しかし、Lombokを使わない場合は、コンストラクタを明示的に定義します。

仕組みとしては同じです。

サービスを受け取るコンストラクタを作り、その中でフィールドに代入します。

@RequiredArgsConstructorを使わない依存性注入の例

ファイル名: PointController.java

package com.example.demo.controller;

import jakarta.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.example.demo.model.PointService;

@Controller
public class PointController {

    // サービスが注入されるフィールド
    private final PointService pointService;

    // 受け取ったサービスをフィールドに代入するコンストラクタ
    public PointController(PointService pointService) {
        this.pointService = pointService;
    }

    @PostMapping("/point/add")
    public String calculatePoint(
            @RequestParam int basePoint,
            @RequestParam int campaignPoint,
            HttpServletRequest request) {

        // サービスに処理を依頼
        int totalPoint = this.pointService.execute(
                basePoint, campaignPoint);

        // 処理結果をリクエストスコープに保存
        request.setAttribute("totalPoint", totalPoint);

        // 結果を表示するビュー名を返す
        return "pointResult";
    }
}

この例では、@RequiredArgsConstructorを使っていません。

その代わりに、PointServiceを引数に持つコンストラクタを自分で書いています。

SpringはPointControllerを作成するときに、PointServiceインスタンスを用意し、このコンストラクタへ渡します。

受け取ったPointServiceは、this.pointServiceに代入されます。

そのため、calculatePointメソッドの中でpointService.executeを呼び出せます。

@RequiredArgsConstructorを使う場合も、自分でコンストラクタを書く場合も、考え方は同じです。

必要なサービスをコンストラクタで受け取り、フィールドに保存して使います。

コンストラクタインジェクションのしくみ

コンストラクタインジェクションは、最初は少し不思議に感じるかもしれません。

newしていないのに、なぜサービスが使えるのかと感じるのは自然です。

仕組みを順番に見ると、次のようになります。

コンストラクタインジェクションの流れ

順番処理
1Springが@Serviceの付いたPointServiceを見つける
2SpringがPointServiceのインスタンスを作成する
3Springが@Controllerの付いたPointControllerを見つける
4PointControllerにはPointServiceを受け取るコンストラクタがある
5SpringがPointServiceインスタンスをコンストラクタへ渡す
6コンストラクタの中でpointServiceフィールドに代入される
7ハンドラメソッド内でpointServiceを利用できる

Springは、@Controllerや@Serviceが付いたクラスを見つけ、必要なインスタンスを管理します。

そして、あるクラスが別のクラスを必要としている場合、必要なインスタンスを用意して渡します。

この仕組みにより、開発者はnewでサービスを作る必要がなくなります。

依存性という言葉の意味

依存性という言葉は、少し難しく感じるかもしれません。

簡単に言えば、あるクラスが別のクラスを使って動いている関係です。

たとえば、PointControllerはPointServiceを使ってポイント計算を行います。

つまり、PointControllerはPointServiceに依存しています。

依存性の例

クラス依存している相手理由
PointControllerPointServiceポイント計算を依頼するため
LoginControllerLoginServiceログイン判定を依頼するため
OrderControllerOrderService注文登録を依頼するため

依存性の注入とは、この依存している相手のインスタンスを、自分で作るのではなく外部から渡してもらう仕組みです。

Springでは、その外部の役割をSpringが担当します。

DIの方法

Springには、依存性の注入を行う方法が複数あります。

この記事では、コンストラクタインジェクションを使いました。

コンストラクタインジェクションは、必要な依存性をコンストラクタで受け取る方法です。

テストしやすく、安全性も高いため、現在よく使われる方法です。

DIの主な方法

方法内容備考
コンストラクタインジェクションコンストラクタで依存性を受け取る基本として押さえたい方法
フィールドインジェクションフィールドに@Autowiredを付ける手軽だが現在は避けられることが多い
セッターインジェクションsetterに@Autowiredを付ける柔軟だが注入し忘れに注意が必要

まずは、コンストラクタインジェクションの形を覚えておくとよいでしょう。

コントローラにはfinalのサービスフィールドを用意する。

@RequiredArgsConstructorを付ける。

サービスには@Serviceを付ける。

この流れを押さえておけば、Springでコントローラからサービスを使う基本形が理解しやすくなります。

Springのモデルとサービスで押さえたいこと

Springでも、データを格納するクラスは通常のJavaクラスとして作成します。

フィールド、コンストラクタ、getter、setterを用意する考え方は、これまでのJavaBeansと大きく変わりません。

ただし、クラスが増えると、getterやsetterなどのボイラープレートコードが多くなります。

Lombokを使うと、@Getter、@Setter、@NoArgsConstructor、@AllArgsConstructorなどのアノテーションによって、定型的なコードを自動生成できます。

アプリケーションの主な処理を担当するクラスは、Springではサービスと呼ばれます。

サービスクラスには@Serviceを付け、Springにサービスとして認識してもらいます。

コントローラからサービスを利用するときは、newでインスタンスを作るのではなく、DIを使います。

特に、コンストラクタインジェクションを使うのが基本です。

@RequiredArgsConstructorを使うと、finalが付いたサービスフィールドを引数にもつコンストラクタをLombokが自動生成してくれます。

そのコンストラクタに対して、Springがサービスのインスタンスを渡してくれるため、コントローラでは自動的にサービスを利用できます。

Springのモデル開発では、データを表すクラス、処理を担当するサービス、そしてそれらを利用するコントローラの役割を分けることが大切です。

この役割分担を意識すると、Spring MVCの構成がぐっと分かりやすくなります。