はじめに
「依存性の注入」——初めてこの言葉を聞いたとき、「何?」と思いませんでしたか?漢字ばかりでいかにも難しそうですし、調べても「DIコンテナが〜」「疎結合が〜」と説明が続いて、余計に混乱した人も多いはずです。
Spring Bootを触っていると、必ずと言っていいほど出てくる @Autowired や @Component。
「おまじないだと思って使っている」「DIって聞いたことはあるけど、結局何が嬉しいのか分からない」という人は多いのではないでしょうか。
この記事では、DI(Dependency Injection)を 「なぜ必要になったのか」という順番 で説明します。いきなり「DIコンテナが〜」と言われてもピンとこないので、まずは"困りごと"から入ります。
対象読者:
- Javaでクラスは書けるけど、Springは使い始めたばかりの人
-
@Autowiredをなんとなくコピペしている人 - 「疎結合」という言葉の意味がふわっとしている人
1. そもそも「依存性」って何?
オブジェクト指向のプログラムは、クラスが他のクラスを呼び出すことで処理を実現しています。
class ClassA {
ClassB b = new ClassB();
void doSomething() {
b.methodA();
}
}
このとき、ClassA は ClassB に 依存している と言います。依存性とは、難しく考える必要はなく、単に「クラス同士の関係性」のことです。
クラス図にすると、こういう関係です。
この関係の何が問題かというと、ClassA の中に new ClassB() と直接書いてしまっているせいで、ClassA は「ClassB そのもの」を知りすぎている状態になっている、という点です。これを 密結合 と呼びます。
密結合の何が困るかというと:
-
ClassBの仕様が変わると、ClassAも書き換える羽目になる -
ClassBを別の実装(例:テスト用のダミークラス)に差し替えたくても、ClassAのコードを直接いじらないと差し替えられない
「一方を直すと、もう一方まで巻き込まれる」——これが密結合の正体です。
2. 第一段階:インタフェースを導入してみる
密結合を解消する定番の手が「インタフェースを間に挟む」ことです。
interface InterfaceI {
void methodA();
}
class ClassB implements InterfaceI {
public void methodA() { ... }
}
class ClassA {
InterfaceI i = new ClassB();
void doSomething() {
i.methodA();
}
}
ClassA が受け取る型を ClassB ではなく InterfaceI にすることで、「InterfaceI を実装しているクラスなら何でもいい」という扱いになります。これで一見、疎結合になった気がします。
クラス図で見ると、ClassA の矢印の先が ClassB ではなく InterfaceI に変わりました。
でも、これだけでは足りない
よく見てください。InterfaceI i = new ClassB(); という行、結局 new ClassB() は残ったまま です。
つまり「どのクラスを使うか」の型は柔軟になったけど、「実際にどのクラスをインスタンス化するか」という決定は、依然として ClassA の中にベタ書きされている。ここがインタフェース導入だけでは解決できない部分です。テスト用にダミー実装へ差し替えたければ、結局 ClassA のコードを書き換える必要があります。
3. 第二段階:DIを導入する
ここでようやくDIの出番です。DIを一言で言うと、
「インスタンス化の実装をなくす仕組み」
です。new ClassB() という行そのものを、クラスの中から消してしまいます。
interface InterfaceI {
void methodA();
}
@Component
class ClassB implements InterfaceI {
public void methodA() { ... }
}
@Component // ← ClassA 自体も DI コンテナの管理対象(Bean)にする必要がある
class ClassA {
@Autowired
InterfaceI i;
void doSomething() {
i.methodA();
}
}
ClassA のコードから new の文字が消えているのが分かるでしょうか。代わりに登場するのが2つのアノテーションです。
ここで1つ注意点があります。@Autowired による注入がうまく働くには、注入される側(ClassB)だけでなく、注入を受け取る側(ClassA)自体もDIコンテナの管理対象(Bean)になっている必要があります。上のコード例で ClassA にも @Component を付けているのはこのためです。もし ClassA に @Component を付け忘れて new ClassA() のように自分でインスタンス化してしまうと、@Autowired は機能せず i は null のままになり、実行時に NullPointerException が発生してしまいます。DIの恩恵を受けるクラスは、自分自身もDIコンテナに委ねる必要がある、と覚えておいてください。
クラス図にすると、ClassA から ClassB への矢印そのものが消えます。ClassA が直接知っているのは InterfaceI だけで、ClassB とのつながりはDIコンテナが裏で仲介する形になります。
| アノテーション | 付与する場所 | 役割 |
|---|---|---|
@Component |
インスタンス化される側のクラス | 「私はDIコンテナに管理してもらいます」という宣言 |
@Autowired |
インスタンスを使いたい変数 | 「ここに、対応するインスタンスを注入してください」という依頼 |
裏側では、Spring Frameworkの DIコンテナ が ClassB のインスタンスをあらかじめ生成しておき、@Autowired が付いた場所に「外部から」差し込んでくれています。この「外から注入される」感覚こそが Dependency Injection(依存性の注入)という名前の由来です。
ClassA はもはや ClassB の存在を一切知りません。知っているのは InterfaceI というインタフェースだけ。これが本当の意味での疎結合です。
💡 実務での補足:コンストラクタ注入について
本記事では理解しやすさを優先してフィールドに直接
@Autowiredを付ける方法(フィールド注入)を紹介しましたが、現在のSpring Frameworkではコンストラクタ注入が推奨されています。@Component class ClassA { private final InterfaceI i; // コンストラクタで受け取る(Spring 4.3以降は@Autowiredも省略可能) public ClassA(InterfaceI i) { this.i = i; } }フィールドを
finalにでき、Springコンテナを起動しなくても単体テストでモックを渡しやすくなるというメリットがあります。まずは「外から注入される」という基本を押さえた上で、慣れてきたらコンストラクタ注入も試してみてください。
4. まとめ:3段階で振り返る
| 段階 | 状態 | 残っている問題 |
|---|---|---|
| ① 素朴な実装 | ClassB b = new ClassB(); |
クラス同士がガッチリ密結合 |
| ② インタフェース導入 | InterfaceI i = new ClassB(); |
型は柔軟になったが new はクラス内に残る |
| ③ DI導入 | @Autowired InterfaceI i; |
new すら書かなくてよい。生成と利用が完全に分離 |
DIとは、突き詰めれば「クラスが『何を使うか』は知っていても、『それをどう作るか』は知らなくてよい状態を作る仕組み」です。この分離ができると、実装をテスト用のモックに差し替えたり、あとから別のクラスに乗り換えたりするのが、呼び出し側のコードを一切変更せずに可能になります。
「@Autowired を書けばSpringが勝手にいい感じにしてくれる」の"いい感じ"の中身が、今回少しでもクリアになれば幸いです。