SpringのDIの仕組みの初歩と実装例

# この記事に書いてあること
- [プロになるためのSpring入門](https://gihyo.jp/book/2023/978-4-297-13613-0)の基本編のDIに関する内容を読んだまとめ
- DIを使ったコードと対応するテストコードの実装例

# DIとは？
Dependency Injection（依存性の注入）の略。
ソフトウェアの設計パターンの一つで、クラス間の依存関係をフレームワーク等で管理して外部から注入する仕組みのこと。[^1]
[^1]:厳密には依存性の注入自体はフレームワークを利用しなくても可能だが、コードが煩雑になるため何らかのフレームワークを使う方が管理しやすい。

# DIのメリット
- 依存オブジェクトの切り替えが簡単になり、コードの再利用性、可読性、テスト容易性が上がる
- 依存オブジェクトをインターフェースにすることでビジネスロジックから低レイヤーへの依存を切り離せる

具体例として保存処理や読み込み処理でServiceクラスがその処理の中でRepositoryクラスを利用してデータベース等に保存や読み込みを行う処理を考えてみます。

この時ServiceクラスはRepositoryクラスに依存していますが、Serviceクラスの処理の中で直接Repositoryクラスをインスタンス化すると差し替えが難しくなります。
差し替えが難しくなると色々な問題が発生します。
- Serviceクラスの単体テストを書く際にRepositoryクラスを動かすデータベースが必要になってテストサイズが大きくなってしまう
- Repositoryの実装が複数ある場合、依存オブジェクトを切り替えるためのロジックがServiceの中に現れて可読性が下がってしまう


またRepositoryの具象クラスを生成するということは、ビジネスロジックがデータベースやファイルシステムなどの低レイヤーの処理が記載されたクラスに依存している状態になります。
こういった良くない状態をDIによって解決することができます。

# SpringでDIを実現する仕組み

## DIコンテナ
SpringでDIの機能を提供するオブジェクトを入れて管理するための入れ物です。
ApplicationContextというインターフェースを実装しているため、ApplicationContextと呼ばれることもあります。

## Bean
SpringのDIが管理するオブジェクトのことをBean[^2]と言います。
ControllerやService、RepositoryなどのクラスをBeanとして指定することでSpringのDIの仕組みにのせて実装することができます。

[^2]:Java Beansとは無関係です。

## コンフィグレーション
DIコンテナに読み込ませる情報のことをコンフィグレーションと言います。
コンフィグレーションには後述のBean定義の情報やDIコンテナの機能を有効化させるかどうかの設定を持っています。
開発者がコンフィグレーションを記述してDIコンテナに読み込ませることで、Beanとなるオブジェクトが生成されて依存オブジェクトがインジェクションされます。
コンフィグレーションは任意のJavaのクラスファイルに `@Configuration` というアノテーションをつけることでDIコンテナにコンフィグレーションとして認識されます。

## Bean定義
クラスをBeanとして指定する方法をBean定義と言います。
Bean定義には以下の3つの方法があります。

### ステレオタイプアノテーション
ステレオタイプアノテーションはBeanとして管理してほしい具象クラスにつけるアノテーションです。ステレオタイプアノテーションが付いた具象クラスはDIコンテナに検知され、オブジェクトの生成や依存関係を管理されます。
ステレオタイプアノテーションにはいろいろな種類があり、対象のクラスに応じて使い分けます。
- `@Service` 
Serviceの具象クラスにつけるアノテーション、付加機能はなし。
- `@Repository`
Repositoryの具象クラスにつけるアノテーション、データベースアクセス例外をSpringが提供する例外に変換できる付加機能が付与される。
- `@Controller`
Controllerの具象クラスにつけるアノテーション、SpringMVCの機能を利用できるようになる付加機能が付与される。
- `@Component`
役割を表さない汎用的なステレオタイプアノテーション、付加機能はなし。

#### コンポーネントスキャン
DIコンテナがステレオタイプアノテーションを付けたBeanを検出する処理のこと。
コンポーネントスキャンを有効にしたコンフィグレーションが読み込まれることでステレオタイプアノテーションを付けられたクラスがDIコンテナに登録されます。

### コンフィグレーションの `@Bean` メソッドで定義する
コンフィグレーションに以下のような記述をすることでDIコンテナにオブジェクトが登録されます。
```Java
@Configuration
public class TestConfig {
  @Bean
  public TestService testService() {
    return new TestService();
  }
}
```
`@Bean` メソッドを使った定義はステレオタイプアノテーションと比べてやや煩雑になるため、大量の定義を行うのは大変ですが、ステレオタイプアノテーションを付与できないライブラリが提供するクラスもBean定義することができます。

### xmlファイルで定義する
古い実装方法になりますが、xmlファイルにBeanの定義を記述することもできます。
```xml
<bean class="com.example.TestService" />
```


## 依存するクラスにBeanを注入する
`@Autowired` というアノテーションを付けることで、依存するクラスにDIが管理しているBeanを注入することができます。
コンストラクタ、Setterメソッド、フィールドにつけることで型の一致するBeanを注入してくれます。
私は特別な理由がない場合コンストラクタによるインジェクションを使った方が良いと考えます。
理由は以下です。
- 循環参照を防止できる
- コンストラクタを使ってインジェクションすることでフィールドにfinal修飾子をつけることができるため、無用な変更を防止できる
- Setterがないクラスの方が外部から利用する際の手続きが減る
Setterを持ったクラスの場合
インスタンス生成⇒必要なオブジェクトをSet⇒主処理のメソッドを呼び出し
Setterのないクラスの場合
インスタンス生成⇒主処理のメソッドを呼び出し

## DIコンテナの生成
DIコンテナの生成は明示的に行う必要があります。
mainメソッドの実行直後等に `ApplicationContext` インターフェースの具象クラス([AnnotationConfigApplicationContext](https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/AnnotationConfigApplicationContext.html)もその一つです)をインスタンス化することで生成されます。

```Java
public static void main(String[] args){
  ApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class);
...
```
SpringBootの場合はSpringApplicationクラスのrunメソッドを使用します。
```Java
public static void main(String[] args){
  ApplicationContext context = SpringApplication.run(TestConfig.class, args);
...
```

## 同じ型のBeanが複数存在した場合の挙動
DIコンテナの生成時にエラーが発生し、処理が中断されます。

## プロファイルを利用したコンフィグレーションの切り替え
プロファイルとはコンフィグレーションをグルーピングするDIコンテナの機能です。
例えば本番環境用のコンフィグレーションとステージング環境のコンフィグレーションであったり、MySQL用のコンフィグレーションとOracle用のコンフィグレーションといった形でコンフィグレーションをグループ分けすることができます。

### プロファイルの使用方法
`@Profile` アノテーションを利用して識別子を付与します。
ステレオタイプアノテーションでの利用例
```Java
@Repository
@Profile("mysql")
public class MySQLTestRepository implements TestRepository{
...
```
```Java
@Repository
@Profile("oracle")
public class OracleTestRepository implements TestRepository{
...
```

### 有効にするプロファイルの指定
システムプロパティや環境変数で指定することができます。

システムプロパティで指定する場合javaコマンドの引数で指定します。

```
java -Dspring.profile.active=mysql TestApplication
```
環境変数で指定する場合は `SPRING_PROFILE_ACTIVE` という環境変数でプロファイルの識別子を指定しておきます。

# テスト用のモックを注入する方法
上述のようにDIを使って実装したプロダクションコードはテストコードを書く際にもDIを利用して注入がしやすい状態になっています。
SpringのDIの仕組みを使ってテスト用のプロファイルを切り替える等してインジェクションをすることもできますが、Mockitoでも仕組みが用意されています。
- `@Mock` `@Spy`
モックとして注入したいインスタンスの変数に付与することで、モックインスタンスを自動生成する。
- `@InjectMocks`
テスト対象のインスタンスを入れる変数に付与することで、モックが注入されたインスタンスを自動生成する。

# 実装例

https://qiita.com/gengen0719/items/bcdda4b8571f5e687051

https://qiita.com/gengen0719/items/73ab46e1a6b46e2cfae7

上記記事で準備したSpringBootの開発環境のサンプルコードをSpringのDIの仕組みを使った実装に書き換える。
ServiceクラスのテストコードもMockitoのインジェクションの仕組みを用いて書く。

`DemoController` のコードを `MessageMakeService` `MessageRepository` に分解。

```Java
package com.example.demo;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class DemoController {

    private final MessageMakeService messageMakeService;

    public DemoController(MessageMakeService messageMakeService){
        this.messageMakeService = messageMakeService;
    }

    @GetMapping("test")
    public String showMessage(Model model) {
        model.addAttribute("message", messageMakeService.make());
        return "test";
    }
    
}
```

```Java
package com.example.demo;

import org.springframework.stereotype.Service;

@Service
public class MessageMakeService {

    private final MessageRepository messageRepository;

    public MessageMakeService (MessageRepository messageRepository){
        this.messageRepository = messageRepository;
    }

    public String make(){
        return "メッセージは : " + messageRepository.loadMessage();
    }
}
```

```Java
package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class MessageRepository {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public String loadMessage(){
        String sql = "SELECT MESSAGE FROM TEST";
        return jdbcTemplate.queryForObject(sql, String.class);
    }
}
```

`MessageMakeService` のテストコードを以下のように書く。
`MessageRepository` をMockに差し替えた単体テストを簡単に記述することができている。

```Java
package com.example.demo;

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 static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.doReturn;

@ExtendWith(MockitoExtension.class)
public class MessageMakeServiceTest {

    @InjectMocks
    private MessageMakeService testTarget;

    @Mock
    private MessageRepository messageRepository;

    @Test
    public void testMake(){
        doReturn("モックのメッセージ").when(messageRepository).loadMessage();
        assertThat(testTarget.make(), is("メッセージは : モックのメッセージ"));
    }

}

```

DIコンテナの生成等は自動生成されるApplicationクラスが実行してくれています。[^3]
[^3]: 詳細は `@SpringBootApplication` の実装を見てみましょう。

```Java
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

}
```

おわり。

追記：

この記事の続きとして以下の記事を書きました。

https://qiita.com/gengen0719/items/c14bea314fd952ca7eb9

SpringSecurityを使ってログインの仕組みを実装する記事も書きました。

https://qiita.com/gengen0719/items/e442feb2c322d633ab63

よろしければこちらもあわせて読んでみてください。