Spring FrameworkにおけるDIのベストプラクティス: コンストラクタインジェクションの優位性

Spring Frameworkでの依存性注入には、

  • フィールドインジェクション
  • コンストラクタインジェクション
  • セッターインジェクション

の3つの方法があります。これらの方法は、オブジェクトが他のオブジェクトに依存している場合に、その依存オブジェクトを自動的に挿入するために使用されます。この中で主に使われるのはフィールドインジェクションとコンストラクタインジェクションです。

Springのコミュニティでは、フィールドインジェクションよりもコンストラクタインジェクションの方が推奨されています。なぜコンストラクタインジェクションが推奨されているのかをフィールドインジェクションのデメリットとコンストラクタインジェクションのメリットの観点から見ていきましょう。

フィールドインジェクション

フィールドインジェクションは、クラスのフィールドに直接依存性を注入する方法です。これは@Autowiredアノテーションをフィールドに適用することで行われます。例えば、

@Component
public class MyService {
    @Autowired
    private DependencyClass dependency;
}

のように書きます。

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

コンストラクタインジェクションは、オブジェクトのコンストラクタを通じて依存性を注入する方法です。コンストラクタに@Autowiredアノテーションを付けることで、Springが自動的に依存オブジェクトを挿入します。例えば、

@Component
public class MyService {
    private final DependencyClass dependency;

    @Autowired
    public MyService(DependencyClass dependency) {
        this.dependency = dependency;
    }
}

のように書きます。

コンストラクタインジェクションが推奨される理由

Spring Framework でコンストラクタインジェクションが推奨される理由のうち、特に理由としてふさわしいと思ったものには以下が2つがあります。

依存コンポーネントの不変性を宣言できる

前述のコード例を見てもらうとわかりますが、フィールドインジェクションではオブジェクト生成時点でフィールドの値が初期化されないため、final宣言をつけることができません。一方、コンストラクタインジェクションではオブジェクト生成時点でフィールドの値が初期化されるため、final宣言をつけることができます。

これにより依存コンポーネントがイミュータブル(不変)であることを明示することができるようになります。

実際にイミュータブルであることで誤って再初期化してしまうことを防ぐことはほとんどないと思いますが、実際に変更してしまうかどうかは別として、変更するつもりがないという設計意図をコードに表現できることが重要だと考えています。

循環依存を回避できる

コンストラクタインジェクションを使用するとコンストラクタの呼び出し時点でインジェクションされてイミュータブルになります。このとき循環依存があると、アプリケーションの起動時にエラーが発生して循環依存を検知することができます。

一方、フィールドインジェクションの場合は実際に対象のフィールドが呼び出されるまで検知できないため、循環依存の発見が遅れたり、発見自体が困難になる場合があります。

単一責任の原則に従っていないことに気づきやすくなるのか

単一責任の原則に従っていないことに気づきやすくなるというメリットを挙げているのを見かけますが、個人的にはそれほど効果がないのではないかと思っています。

まったくないとは言いませんが、Lombokを使用していると、ほぼフィールドインジェクションのような使い心地でコンストラクタインジェクションを使えてしまうので、レビューで検出する方が確実だと思います。例えば、

@Component
@RequiredArgsConstructor
public class MyService {
    private final DependencyClass dependency;
}

のように@RequiredArgsConstructorアノテーションを使用することで、コンストラクタを省略することが可能です。これ自体はボイラープレートコードを書かなくて済むというメリットがありますが、コンストラクタ自体を書かないため、依存コンポーネントが多すぎることに気づきにくいと思います。

コンストラクタインジェクションに頼るのではなく、きちんとレビュー前のセルフチェックやレビューで依存関係の複雑さや単一責任の原則に従っているのかなどをチェックすることが重要だと考えています。

単体テストコードを書きやすくなるのか

元々はコンストラクタインジェクションの方が書きやすかったと思うのですが、現在ではほとんど変わらないと思いました。

フィールドインジェクションを使ったコンポーネントの例

フィールドインジェクションを使ったコンポーネントのテストを書く例として、次のようなサービスクラスに対するテストケースを考えてみましょう。

package com.example.demo;

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

@Service
public class SampleService {

    @Autowired
    private DummyComponent dummyComponent;

    public String doSomething() {
        return dummyComponent.doSomething();
    }
}

このサービスクラスに対するテストは以下のようになります。

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.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class SampleServiceTest {

    @InjectMocks
    private SampleService sampleService;

    @Mock
    private DummyComponent dummyComponent;

    @Test
    public void testDoSomething() {
        when(dummyComponent.doSomething()).thenReturn("test");

        assertEquals("test", sampleService.doSomething());
    }
}

コンストラクションインジェクションを使ったコンポーネントの例

コンストラクタインジェクションを使ったコンポーネントの例も見てみましょう。同じサービスクラスをコンストラクタインジェクションで書き換えた場合、以下のようになります。

package com.example.demo;

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

@Service
public class SampleService {

    private final DummyComponent dummyComponent;

    @Autowired
    public SampleService(DummyComponent dummyComponent) {
        this.dummyComponent = dummyComponent;
    }

    public String doSomething() {
        return dummyComponent.doSomething();
    }
}

このサービスクラスに対するテストは以下のようになります。

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.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class SampleServiceTest {

    @InjectMocks
    private SampleService sampleService;

    @Mock
    private DummyComponent dummyComponent;

    @Test
    public void testDoSomething() {
        when(dummyComponent.doSomething()).thenReturn("test");

        assertEquals("test", sampleService.doSomething());
    }
}

見ての通り同じテストケースクラスでテストできます。

現状ではテストのしやすさという点ではその差はほぼないと考えて差し支えないと思います。

まとめ

依存コンポーネントに対して不変性を明示でき、循環依存を回避できるようになる点を考慮すると、コンストラクタインジェクションを積極的に使用していくことは重要だと思います。

一方で、単一責任の原則に従っているかどうか、依存関係の多いファットなコンポーネントになっていないかどうかについては、ペアプログラミング時に議論したり、セルフチェックやレビューによってチェックすることが重要だと思います。

[Spring Boot]IntelliJ IDEAでLombokを使っていてエラーになる場合の対処方法

以下の記事に解決方法が載っていました。

https://stackoverflow.com/questions/72583645/compile-error-with-lombok-in-intellij-only-when-running-build

遭遇した事象

Spring Initializrからダウンロードした時点でLombokは依存関係に追加されています。

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.0.4'
	id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.t0k0sh1'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.projectlombok:lombok:1.18.22'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

IntelliJ IDEAでLombokを使用可能にするためには、設定のビルド、実行、デプロイ>コンパイラー>アノテーションプロセッサーで、「アノテーション処理を有効にする」にチェックをつけ、「プロジェクトクラスパスからプロセッサーを取得する」にチェックがついている状態にします。

ここまでがよく知られているLombokの導入方法になります。

lombok.Dataアノテーションを定義したクラスを作成し、

package com.t0k0sh1.tutorial.entity;

import lombok.Data;

@Data
public class User {
    private Long id;
    private String name;
    private String email;
    private String password;
}

自動生成されているであろうgetterを使ってみます。

package com.t0k0sh1.tutorial.controller;

import com.t0k0sh1.tutorial.entity.User;
import org.springframework.web.bind.annotation.*;

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

@RestController
public class UserController {
    private final List<User> users = new ArrayList<>();

    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        return users.stream().filter(a -> a.getId().equals(id)).findFirst().orElse(null);
    }

}

すると、getterが見つからずにコンパイルエラーとなります。

/Users/t0k0sh1/Workspace/tutorial/src/main/java/com/t0k0sh1/tutorial/controller/UserController.java:15: エラー: シンボルを見つけられません
        return users.stream().filter(a -> a.getId().equals(id)).findFirst().orElse(null);
                                           ^
  シンボル:   メソッド getId()
  場所: タイプUserの変数 a

対処方法

build.gradleを以下のように書き換えることで問題を解消することができます。

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.0.4'
	id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.t0k0sh1'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.projectlombok:lombok:1.18.22'
	implementation 'org.modelmapper:modelmapper:3.1.1'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	// 以下の1行を追加する
	annotationProcessor 'org.projectlombok:lombok:1.18.22'
}

tasks.named('test') {
	useJUnitPlatform()
}

追加でannotationProcessor 'org.projectlombok:lombok:1.18.22'を追記します。(バージョン部分は元の記述に合わせてください)

Gradleプロジェクトの再ロード(右端のGradleタブを開いて更新ボタンをクリックする)し、プロジェクトのビルドを行なってください。

すると、先ほどまでエラーとなっていましたが、今度はビルドに成功します。

[Spring Boot]プロジェクトを作成する(Spring Initializr)

Spring Bootのプロジェクトの作成方法はいくつかありますが、本記事ではSpring Initializrを使って作成します。

Projectを選択する

執筆時点では「Gradle – Groovy」「Gradle – Kotolin」「Maven」から選択できます。使いたいものを選択してください。

ここでは「Gradle – Groovy」を選択しています。

Languageを選択する

執筆時点では「Java」「Kotolin」「Groovy」から選択できます。使いたいものを選択してください。

ここでは「Java」を選択しています。

Spring Bootのバージョンを選択する

Spring Bootのバージョンを選択してください。

ここでは「3.0.4」を選択しています。

Project Metadataを入力する

プロジェクトのメタデータを入力します。

内容はプロジェクトに合わせて設定してください。

Dependenciesを選択する

Dependenciesの選択は任意ですが、Webアプリケーションのプロジェクトを作成するので、Spring Webだけ追加しておきます。すでに使用するものが決まっている場合はそれも追加しておいてください。

プロジェクトをダウンロードする

GENERATEボタンをクリックしてプロジェクトをダウンロードしてください。

プロジェクトを展開・配置してIDEで開く

ではダウンロードしたプロジェクトを展開・配置して、IDEで開いてみましょう。ここではIntelliJ IDEA Ultimateを使用しています。

まずはダウンロードしたZIPファイルを展開し任意のフォルダに配置します。

$ unzip ~/Downloads/tutorial.zip -d ~/Workspace/

次にIDEでプロジェクトを開きます。

動作確認のためにアプリケーションを実行してみましょう。

アプリケーションが起動したらhttp://localhost:8080/にアクセスします。

Spring BootではMainメソッド持ったクラス以外何もないため、表示するページがなく、エラー画面が表示されますが、この画面が表示しているのであれば問題ありません。

モバイルバージョンを終了