3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Spring Boot】DI(Dependency Injection)を「テスト」と「着せ替え」の視点で理解する

3
Posted at

Spring Bootを扱う上で避けて通れないのが「DI(Dependency Injection)」です。DIとは、「コードを修正せずに部品を付け替えるための仕組み」です。

DIを学ぶ価値は、「変更とテストに強い設計」の視点を得ることにあります。「保守性を高めるために、疎結合にする」という意図を持ってDIを扱えるようになりましょう。本記事では、DIがどのような仕組みで動き、どう楽にしてくれるのかを、コードを交えて解き明かしていきます。

1. DI(依存性の注入)とは?

DIとは「コードを修正せずに部品を付け替えるための仕組み」です。別の言い方をすると、「オブジェクトの準備を自給自足(new)からデリバリー(注入)に変えること」です。例えば、あるクラスの中で別のクラスを使いたいとき、new してインスタンスを作ります。しかし、これが「密結合」を生み出します。

コードで比較してみましょう。

【Before】DIを使わない「密結合」なコード

この場合、UserService がないと UserController は動きません。密結合になっています。

public class UserController {
    // 自分でインスタンスを生成(自給自足)
    private final UserService userService = new UserService();

    public void registerUser() {
        userService.save();
    }
}

【After】DIを使った「疎結合」なコード

一方、DIを使うとこうなります。UserController は自分では new せず、「誰かが外から渡してくれる」のを待つスタイルに変わります。

@RestController
public class UserController {
    private final UserService userService;

    // コンストラクタで外から受け取る(デリバリー)
    public UserController(UserService userService) {
        this.userService = userService;
    }
}

この「外から渡してもらう」ことこそが、依存性の注入(DI)の正体です。仕組みや利用方法は後述します。

2. DIがもたらす2つのメリット

DIを採用する理由は、システムに「柔軟性」を持たせることにあります。具体的にどのような恩恵があるのか、2つのポイントを解説します。

メリット①:テストの容易性

DIの1つ目のメリットは、「テストが書きやすくなること」です。逆にDIを使わないコードの単体テストは、外部環境に振り回されることになります。まず、DIを使用しないテストを見ていきましょう。new でインスタンスを作るとまずい理由は、「利用する側が、部品の生成まで行ってしまうから」です。

public class UserController {
    // クラスの中で直接 new(自給自足 = 部品が固定されている)
    private final UserService userService = new UserService();

    public void registerUser() {
        // テストのたびに、実際にデータベースへ保存されてしまう
        userService.save();
    }
}

このコードでは、UserController をテストしたいだけなのに、「本物のデータベース操作を行う UserService」が強制的にセットでついてきます。 new と書かれている以上、テスト側から中身を入れ替える手段がなく、テスト用のDB準備など(外部環境の準備)が必要になり、手間がかかります。

次に、DIを使用するテストを見ていきましょう。DIの本質は、部品の生成をクラス内部で行わず、「必要な部品を外から引数として受け取る構造にする」ことです。

実装としては、コンストラクタで部品を受け取るようにします。ちなみに、インターフェースを型に指定してコンストラクタで受け取ることで、規格を満たすクラスなら何でも受け取れるようになります。

@RestController
public class UserController {
    private final UserService userService; // インターフェースに依存させる

    // Springが自動で適切な実装クラスを探して渡してくれる
    public UserController(UserService userService) {
        this.userService = userService;
    }

    public void registerUser() {
        userService.save();
    }
}

テストコードでは、Mockitoを利用します。モックのuserServiceクラスを作成し、userControllerクラスをインスタンス化し、コンストラクタ経由でモックのuserServiceクラスを注入してます。そして、userControllerクラスをテストしてます。

@ExtendWith(MockitoExtension.class)
class UserControllerTest {
    @Mock
    private UserService userService; // 偽物の部品を作成

    @InjectMocks
    private UserController userController; // コンストラクタ経由でモックを注入

    @Test
    void ユーザー登録が呼び出されること() {
        userController.registerUser();
        // 検証:本物のDBには繋がらず、メソッドが呼ばれたことだけを確認できる
        verify(userService, times(1)).save();
    }
}

このテストのポイントは、「UserController が正しく UserService に仕事を依頼したか」だけをチェックしている点です。「UserService がDBに保存処理をしたか」は気にしません。DIのおかげで、部品をモック(ニセモノ)にすり替えられるため、「相手(部品)の都合に左右されず、自分のクラスの責任だけをテストできる」ようになるのです。

メリット②:環境の柔軟な切り替え

もう一つの大きなメリットが、「環境に応じた挙動の切り替え」です。Springの「プロファイル機能(Spring Profiles)」を使えば、ソースコードを変えずに起動設定ひとつで部品を付け替えられます。

例えば、ユーザー登録時のメール送信機能の「本番用」と「開発・モック用」の使い分けを考えます。ローカル開発中に本物のメールが飛んでしまうのは避けたいです。ここでインターフェースによる切り替えが効いてきます。以下3つのserviceがあるとします。

  • インターフェース: MailService(メールを送る約束)
  • 本番用Bean: RealMailService(実際に送信)
  • 開発用Bean: FakeMailService(ログを出すだけ)

プロファイル機能(@Profile)による切り替えを行います。@Profile アノテーションで、どの環境でどのBeanを有効にするか指定できます。

@Service
@Profile("prod") // 本番環境でのみ有効
public class RealMailService implements MailService { ... }

@Service
@Profile("dev")  // 開発環境でのみ有効
public class FakeMailService implements MailService { ... }

あとは application.properties の設定を書き換えるだけで、DIコンテナが自動的に適切なBeanを選んで注入してくれます。

spring.profiles.active=dev

このように環境を切り替える柔軟性にはメリットがあります。まず、安全性の確保ができます。「誤ってメールを実際に送る」といった事故を物理的に防げます。また、開発効率が向上します。外部システムの完成を待たずに、自前のモックでロジックを先行開発できます。本番と開発で部品を自在に付け替えるという疎結合な設計が、変更に強いシステムを作ることになります。

3. 「DI」の仕組み

ここまで、DIを使うと「外から部品が届く」という話をしてきましたが、「誰が」その部品を作り、管理し、届けているのでしょうか?その正体はSpring Bootの司令塔である「DIコンテナ」です。DIコンテナがどのように動いているのか、整理していきましょう。

まず、DIコンテナを理解する4つのキーワードがあります。これらは「レストラン」に例えると分かりやすくなります。

用語 役割 レストランに例えると
Bean DIコンテナに管理されている「インスタンス」そのもの。 料理(実際に提供されるもの)
Bean定義 「どのクラスをどう作るか」という「設計図」。 レシピ(作り方の手順書)
Application Context Beanを保持し、必要な場所へ配送する「コンテナ本体」。 倉庫
コンフィグレーション 外部ライブラリ等をBeanとして登録するための「指示書」。 仕入れ・特別な調理指示

基本用語がわかったところで全体像を見ていきましょう。DIコンテナは、アプリケーション起動時に以下のステップで「部品の準備と配送」を行います。

  1. 設計図の読み込み: アノテーションや設定クラスから「Bean定義」を読み込む。
  2. Beanの生成: 設計図をもとにインスタンス(Bean)を作成する。
  3. 保管: 作成したBeanを「Application Context」という倉庫に保管する。
  4. 注入: 必要としているクラス(@Autowired やコンストラクタがある場所)へ配送する。

ちなみに、自分で作ったクラスであれば後述するアノテーションで自動登録できますが、ライブラリなどの「他人が作ったクラス」はソースをいじれません。そこで必要になるのが、先ほどの用語にあった「コンフィグレーション(指示書)」による手動登録です。

【コード例】コンフィグレーションによるBean登録

例えば、外部ライブラリの RestTemplate をDIで使いたい場合は、以下のように記述します。

@Configuration // このクラスはBeanの指示書であることを宣言
public class AppConfig {

    @Bean // メソッドの戻り値をBeanとしてDIコンテナに登録
    public RestTemplate restTemplate() {
        // ここでnewしたインスタンスが「倉庫」に保管される
        return new RestTemplate();
    }
}

これにより、ライブラリのクラスをDIコンテナの管理下に置くことができます。つまり、アプリケーション起動時に「Bean定義」を読み込み、インスタンス(Bean)を作成し、Application Contextに保存します。あとは、開発者が利用したいシーンで注入するだけです。

Beanはデフォルトで「シングルトン」

ここで、DIコンテナが管理するBean(インスタンス)の性質について触れておきます。Spring Bootにおいて、Beanはデフォルトで「シングルトン」として扱われます。

シングルトンとは、「アプリケーション内でそのインスタンスがたった1つしか存在しないこと」を意味します。newする場合、new を呼ぶたびに新しいインスタンスがメモリ上に作られます。
一方、DIする場合は、どこで注入(インジェクション)しても、DIコンテナの倉庫にある「同じインスタンス」が使い回されます。同じインスタンスを使いまわす前提として、シングルトンが重要になります。

これによりメモリ消費を抑え、パフォーマンスを最適化しています。ただし、1つのインスタンスを全員で共有するため、Bean(Serviceなど)のフィールドに「状態(書き換わるデータ)」を持たせてはいけないというルールが生まれます。

4. DIの利用方法

前章までは「DIとは何か」を解説しましたが、ここからは「どう使うか」を解説します。開発者は、Beanを登録し、利用したい箇所で注入すればよいです。

役割を宣言して自動登録

プロジェクト内のすべてのクラスを @Bean で手動登録するのは現実的ではありません。そこで、クラスに特定のアノテーションを付与するだけでSpring Bootが自動的にBeanとして登録してくれる「ステレオタイプアノテーション」を活用します。

システム内での役割に応じて、以下の4つを使い分けます。クラス名に以下のようなアノテーションをつけます(コード例は1章を参照)。

アノテーション 役割 説明
@Controller / @RestController 入口 Webリクエストを受け付ける。画面遷移やAPIの制御を行います。
@Service ロジック ビジネスロジックを担当。計算処理やデータ操作をまとめます。
@Repository DB データベース操作を担当する。データの保存・検索を担います。
@Component 汎用 上記のいずれにも当てはまらない便利アノテーション。汎用的な部品に使われます。

ちなみに、 @Service などの中身はすべて @Component です。役割を分かりやすくラベル付けすることで、人間にもSpringにもそのクラスの目的を伝えています。

アノテーションを付けたクラスがなぜBeanになるのでしょうか。その裏側にあるのが「コンポーネントスキャン」です。

Spring Bootの起動クラス(@SpringBootApplication 付与クラス)があるパッケージを起点に、その配下にあるクラスをすべてスキャンし、アノテーションが付いたものを自動で「Bean定義」として読み込みます。注意として、 起動クラスより「上の階層」にクラスを置くと、デフォルトではスキャンされず、Beanとして認識されません。「DIできない!」ときは、このスキャン範囲外にクラスを置いていることが疑いましょう。

注入の作法

Beanを登録したら、次は「注入」です。Springには主に3種類の注入方法がありますが、「コンストラクタインジェクション」を推奨します。

①フィールドインジェクション
フィールドに直接 @Autowired をつける方法です。

@Autowired
private UserService userService;

手軽ですが、Javaの言語仕様上 final(不変)にできません。また、DIコンテナがないと値をセットできないため、単体テストが面倒になります。

②Setterインジェクション
Setterメソッド経由で注入します。「あってもなくても動く」という任意の依存関係に使われますが、業務システムでは部品が欠けると動かないことが多いため、あまり使われません。

③コンストラクタインジェクション
コンストラクタの引数で部品を受け取る方法です。

private final UserService userService; // finalが使える!

public UserController(UserService userService) {
    this.userService = userService;
}

コンストラクタインジェクションを推奨するには理由があります。まず、final を付与でき、起動後に中身が書き換わるバグを物理的に防げます。また、テストがやりやすくなります。new UserController(mockService) と呼び出すだけで済み、Springを起動せずテストが書けます。さらに、循環参照を早期検知できます。依存がループしている場合、アプリ起動時にエラーで教えてくれるため、本番障害を未然に防げます。

実務の定番形と使い分け

コンストラクタを書く手間は Lombok で解決します。@RequiredArgsConstructor を使うと、final フィールドを引数に持つコンストラクタを自動生成してくれます。

@RestController
@RequiredArgsConstructor // これだけでコンストラクタを自動生成!
public class UserController {
    private final UserService userService; // finalをつける
}

Lombokと組み合わせれば、@Autowired を書かずに安全でテストしやすいDIが実現します。

最後に、シングルトンの最後でもふれた「ルール」について触れます。ルールとは「Beanにするか、newを使うか」の使い分けのルールです。

結論、すべてのクラスをBeanにする必要はありません。「使い回す道具はBean、使い捨てる材料はnew」が判断基準です。

カテゴリ 具体例 扱い 理由
ロジック・機能 Service, Repository DIする(Bean) 状態を持たず使い回したい「道具」であり、差し替えの必要があるから。
データ・状態 DTO, Entity newする ユーザーごとに中身が異なる「材料」であり、DI管理の必要がないから。

この区別ができるようになると、メンテナンス性が向上します。

5. おわりに

DIを採用する目的は、「疎結合」な設計を実現することにあります。特定のクラスを直接 new する密結合なコードは、一部の修正が全体に波及するリスクを孕みます。DIという壁を挟むことで、部品同士が互いに依存しすぎない状態をつくれます。この状態が「変化への強さ」につながります。「DI」とは何か聞かれたときに、うまく説明できなかったので、整理し直しました。誰かの役に立ったらうれしいです。

本稿は、生成AIと壁打ちして記載しています。

最後まで読んでいただいた方、ありがとうございました。

参考文献

土岐孝平(2023年7月12日)「プロになるためのSpring入門ーーゼロからの開発力養成講座」, 技術評論社.

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?