i-Vinci TechBlog
株式会社i-Vinciの技術ブログ

Spring DI/DI コンテナについて

案件で SpringBoot を使用した開発をすることになりました。

作業していく中で DI(Dependency Injection)の考え方に詰まりました。

SpringBoot を使用し半年が経ち自分なりに DI の概念をある程度落とし込めたので、

調査結果を備忘録として残してみました。

目次

  1. DI とは
  2. DI コンテナとは
  3. DI コンテナのメリット
  4. コンポーネントスキャン
  5. DI コンテナの設定
    1. Bean 定義
    2. DI の記述
  6. テスト例
  7. まとめ

DI とは

Dependency Injection の略で日本語で言うと「依存性の注入」です。
Wikipedia で調べてみると「オブジェクトの注入」と書かれていました。(こちらのがわかりやすいですね)
クラスの外から依存性(オブジェクト)を注入することで、クラス間の依存関係を解決する目的を持っているそうです。

DI コンテナとは

DI を実現するためのフレームワーク。
DI コンテナを経由してインスタンスを生成、管理する。
※Spring では DI コンテナが管理するインスタンスのことを「コンポーネント」と表現します。

・DI コンテナイメージ図

出典:Spring 徹底入門

DI コンテナのメリット

  • インスタンスのスコープ管理が可能
  • インスタンスのライフサイクル制御が可能
  • 共通機能を組み込むことが可能
  • テストが簡単になる ※6.テストの実施例を記載しています。

    コンポーネント間が疎結合となり、単体テストを行いやすくなる。

    1. DIの作法に従うことで、クラス内部でのインスタンス生成を行わなくなる(密結合の回避)

    2. インスタンスは外部から注入されるため、参照の切り替えが容易。

    3. 2.の要素により、テストコードでは注入するクラスをモックに置換すれば良いため、
      クラス結合観点の確認が容易となる。

コンポーネントスキャン

コンポーネントスキャンとは、クラスローダーをスキャンして特定のクラスを自動的に DI コンテナに登録すること。
今回はコンポーネントスキャンの対象としてよく使う@Controller,@Service,@Component,@Repository について記載。

アノテーション説明
@ControllerMVC パターンの C の役割を担うコンポーネントで、
このアノテーションを付与したコンポーネントではクライアントからのリクエスト/レスポンスに関わる処理をする。
・@RestController との違い
大まかに View に遷移するかしないかの違いがあり、
@Controller は戻り値として View(HTML)を返すのに対して@RestController はリクエストを受け、JSON や XML を返す違いがある。
@Serviceビジネスロジックを実装するコンポーネント。
@Repositoryデータの永続化に関わる処理を提供するコンポーネント。ORM などを利用して、データの CRUD 処理を実装する。
@Component上記 3 つに当てはまらないコンポーネント。ユーティリティクラスなどに付与する。
thymeleaf のテンプレートの機能だけでは賄いきれない HTML への変換ロジックを使いたい時などに便利。

他にもデフォルトで以下のアノテーションが付いたクラスが対象になる。

  • @Configuration
  • @RestController
  • @ControllerAdvice
  • @ManagedBean
  • @Named

DI コンテナの設定

1. Bean 定義

Spring では DI コンテナに登録するコンポーネントのことを「Bean」、Bean の構成を定義することを「Bean 定義」と言います。
今回はアノテーションベースの定義で書かせていただきます。(XML ベース,Java ベースの定義は今回は扱いません。)
Bean 定義ファイルに記述するのではなく、DI コンテナに管理させたい Bean もしくはコンポーネントにアノテーションを付与し、
読み込むことで DI コンテナに登録する方法です。
記述方法は以下です。
・Bean クラスに@Component アノテーションを付与して、コンポーネントスキャンの対象にします。

@Component
public class SampleController {

2. DI の記述

DI の記述方法の種類は以下の 3 つがあります。

  • セッターインジェクション
    セッターの引数に対して DI する方法。
    • メリット :テスト時にモックオブジェクトに置き換えやすい
    • デメリット:依存する Bean が多いと setter が多くなる
@Controller
public class SampleController {

    // 利用するBeanのフィールドを定義する
    private SampleService SampleService;

    // Bean設定用のsetterを@Autowiredを付与して定義する
    @Autowired
    public void setSampleService(SampleService sampleService) {
        this.sampleService = sampleService;
    }

    public void fuga() {
        sampleService.hoge();
    }
}
  • コンストラクタインジェクション
    コンストラクタの引数に対して DI する方法。
    • メリット :フィールドに final 修飾子を付けて不変に出来る。
    • デメリット:依存する Bean が増えるにつれて引数が多くなる
@Controller
public class SampleController {

    // 利用するBeanのフィールドを定義する
    private final SampleService sampleService;

    // @Autowiredを付与したコンストラクタを用意する(引数はBeanオブジェクト)
    // コンストラクタが1つの場合、@Autowiredは省略可
    @Autowired
    public SampleController(SampleService sampleService) {
        this.sampleService = sampleService;
    }

    public void hoge() {
        // Beanを利用する
        sampleService.foo();
    }
}
  • フィールドインジェクション
    クラスのフィールドに対して DI する方法。
    • メリット :コード量が少なくて一番シンプルに書ける。
    • デメリット:フィールドを final 化できない
@Controller
public class SampleController {

    // 利用するBeanのフィールドを@Autowiredを付与して定義する
    @Autowired
    private SampleService sampleService;

    public void hoge() {
        // Beanを利用する
        sampleService.foo();
    }
}

テスト例

以下に例を記載します。

例  Repository がまだ実装されていない段階での Service のテスト

パッケージ構成

TaskController.java

package com.example.demo.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.demo.service.TaskService;

@Controller
@RequestMapping("/task")
public class TaskController {
  @Autowired
  private TaskService taskService;

  @GetMapping
  public String task(Model model) {
     model.addAttribute("taskList", taskService.findAll());
     return "index";

  }
}

Task.java

package com.example.demo.dto;

import java.time.LocalDateTime;

import lombok.Data;

@Data
public class Task {
  private int id;
  private int typeId;
  private String title;
  private String detail;
  private LocalDateTime deadline;
  private int userId;
}

TaskDao.java

package com.example.demo.repository;

import java.util.List;

import com.example.demo.dto.Task;

public interface TaskDao {

  List<Task> findAll();
}

今回のテスト対象クラス

TaskService.java

package com.example.demo.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.demo.dto.Task;
import com.example.demo.repository.TaskDao;

@Service
public class TaskService {

  @Autowired
  private TaskDao dao; /** 実装クラスが未実装*/

  public List<Task> findAll() {
    return dao.findAll();
  }

}

テストクラス

TaskServiceTest.java

package com.example.demo.service;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.example.demo.dto.Task;
import com.example.demo.repository.TaskDao;

@ExtendWith(MockitoExtension.class)
@DisplayName("TaskServiceImplのテスト")
public class TaskServiceTest {

  @Mock
  private TaskDao dao;

  @InjectMocks
  private TaskService taskService;

  @Test
  @DisplayName("テーブルtaskの全件取得で0件の場合のテスト")

  void testFindAllList() {

    List<Task> list = new ArrayList<>();

    when(dao.findAll()).thenReturn(list);

    List<Task> actualList= taskService.findAll();

    verify(dao, times(1)).findAll();

    assertEquals(0, actualList.size());
  }
}

TaskService.java で TaskDao を@Autowired することにより、TaskDao のインスタンスは常に1つだけしか存在しない状態(=シングルトンパターン)での実装となります。

シングルトンパターンについては今回の記事の主題とは異なりますので扱いません。ご興味のある方は下記の参考資料をご覧ください。

まとめ

今回は DI,DI コンテナについて書かせていただきました。
これから Spring Framework を使おうと考えている方の参考になれば幸いです。

引用元