こんにちは、かつコーチです。
DIの基本的な考え方、Controller・Service・Repositoryの役割、そしてThymeleafでの画面作成まで見てきました。
ここまでの記事ではDIの実装方法として@Autowiredをさらっと使ってきましたが、実はDIには複数のやり方があり、Spring公式はコンストラクタインジェクションを推奨しています。
この記事では、フィールドインジェクション(@Autowiredをフィールドに付ける方法)とコンストラクタインジェクションの違いと、実務でどちらを選ぶべきかを整理します。
DIのインジェクション方法とは?
3つのインジェクション方法
Spring BootでDIを行う方法には、主に次の3種類があります。
- フィールドインジェクション:フィールドに直接
@Autowiredを付ける - セッターインジェクション:セッターメソッドに
@Autowiredを付ける - コンストラクタインジェクション:コンストラクタの引数として依存を受け取る
いずれも最終的にはSpringのDIコンテナが必要なBeanを注入してくれますが、書き方によって安全性や保守性に差が出ます。
なぜ書き方によって差が出るのか
依存性注入(DI)とは、クラスが必要とする部品(依存)を、クラス自身がnewで作るのではなく外部から渡してもらう仕組みです。
「外部から渡してもらう」タイミングと方法が、フィールド代入・セッター呼び出し・コンストラクタ呼び出しのどれになるかで、Javaの言語仕様上の制約(finalが使えるか、必須項目として強制できるか)が変わってきます。
この違いが、後述するテストのしやすさや不変性の担保に直結します。
基本の書き方・実装手順
手順1:フィールドインジェクションの書き方
これまでの記事でも使ってきた、最もシンプルな書き方です。
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> findAll() {
return userRepository.findAll();
}
}
フィールドに直接@Autowiredを付けるだけで動くため書きやすい反面、後述する理由から推奨されていません。
手順2:コンストラクタインジェクションの書き方
推奨されるコンストラクタインジェクションは、以下のように書きます。
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> findAll() {
return userRepository.findAll();
}
}
コンストラクタが1つしかない場合、Spring Boot 4.3以降(Spring Framework 4.3以降)では@Autowiredアノテーション自体を省略できます。
フィールドをfinalで宣言できるため、「一度セットされたら変更されない」という不変性をJavaのコンパイラレベルで保証できるのが大きな利点です。
手順3:Lombokで記述量を減らす
コンストラクタインジェクションは記述量が増えるのが唯一の弱点ですが、Lombokの@RequiredArgsConstructorを使うと解消できます。
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public List<User> findAll() {
return userRepository.findAll();
}
}
finalフィールドを引数に取るコンストラクタが自動生成されるため、フィールドインジェクションと変わらない見た目のまま、コンストラクタインジェクションの恩恵を受けられます。
つまずきやすい設定・注意点
依存が複数ある場合、コンストラクタの引数が増えていきます。
引数が4〜5個を超えてきたら、「そのクラスが責務を持ちすぎていないか」を見直すサインだと捉えるとよいでしょう。
よくあるつまずきポイント・エラー対処
フィールドインジェクションのままテストを書こうとして困る
フィールドインジェクションの最大の問題は、単体テストが書きにくいことです。
❌ Before
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// ...
}
このクラスを純粋なJavaのテスト(Spring DIコンテナを使わないテスト)で検証しようとすると、userRepositoryフィールドはprivateかつ初期化する手段がないため、new UserService()しただけではuserRepositoryがnullのままになります。
java.lang.NullPointerException:
Cannot invoke "UserRepository.findAll()" because
"this.userRepository" is null
筆者も最初にサービス層の単体テストを書いた際、モックを@Autowiredのフィールドにどう渡せばいいのか分からず、結局ReflectionTestUtilsでリフレクションを使ってフィールドを無理やり書き換えるという、明らかに遠回りな方法を取ってしまった経験があります。
✅ After
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// ...
}
コンストラクタインジェクションであれば、テストコード側でnew UserService(mockUserRepository)のように、モックをコンストラクタ引数として直接渡すだけで済みます。
Spring DIコンテナを起動しない軽量なテストが書けるようになり、テストの実行速度も大きく改善します。
応用・一歩先の使い方
コンストラクタインジェクションには、循環参照(AがBを必要とし、BもAを必要とする状態)をアプリケーション起動時に確実に検知できるという利点もあります。
フィールドインジェクションでは実行時まで気づかないケースがある一方、コンストラクタインジェクションではBeanCurrentlyInCreationExceptionとして起動時点でエラーになるため、設計上の問題を早期発見できます。
実務のSpring Bootプロジェクトでは、フィールドインジェクションを使わず、@RequiredArgsConstructor+コンストラクタインジェクションを標準スタイルとするチームがほとんどです。
まとめ
この記事のポイント
- インジェクション方法にはフィールド・セッター・コンストラクタの3種類がある
- コンストラクタインジェクションは
finalによる不変性の担保とテストのしやすさで優れる - Lombokの
@RequiredArgsConstructorを使えば記述量の弱点も解消できる - 循環参照もコンストラクタインジェクションなら起動時に検知できる
次に読むべき記事
DI・MVCの基礎が身についたら、次はSpring Data JPAを扱う「Spring Data JPAの基本とEntityの定義」で、データベース連携に進んでいきましょう。
タグ: Spring Boot, 中級者向け, フレームワーク基礎