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コンテナは、アプリケーション起動時に以下のステップで「部品の準備と配送」を行います。
- 設計図の読み込み: アノテーションや設定クラスから「Bean定義」を読み込む。
- Beanの生成: 設計図をもとにインスタンス(Bean)を作成する。
- 保管: 作成したBeanを「Application Context」という倉庫に保管する。
- 注入: 必要としているクラス(
@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入門ーーゼロからの開発力養成講座」, 技術評論社.