SJC共同開発推進室の趙(ちょう)です。
Spring Bootを使い始めた際、@Autowiredと書くだけでインスタンスが自動で使えるようになることに、戸惑いと同時に「魔法みたいだ」と感じた方も多いのではないでしょうか。
この「魔法」の正体こそ、Springフレームワークの根幹をなすDI(依存性の注入)です。
本記事では、DIが「なぜ必要なのか」を理解するために、設計の基本である密結合と疎結合から掘り下げます。その上で、SpringのDIコンテナが具体的にどのような仕組みで Bean を生成し、@Autowired に応えて注入しているのかを、ステップバイステップで徹底的に解説します。
DIを深く理解することは、テストしやすく、変更に強いコードを書くための大きな一歩です。ぜひ最後までお付き合いください!
1. 密結合(Tight Coupling)とは?
密結合とは、あるクラス(部品A)が、別のクラス(部品B)の「具体的な中身」に強く依存してしまっている状態を指します。
-
比喩: 「特定のメーカーの、特定のネジ(部品B)でしか組み立てられない家具(部品A)」。
悪いコード例:『車』が『エンジン』を直接作る
例えば、「車(Car)クラス」が「エンジン(Engine)クラス」を使う場合を考えます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// エンジンクラス public class Engine { public void start() { System.out.println("エンジン始動"); } } // 車クラス(これが密結合の例) public class Car { // Carクラスが「自分で」「具体的なEngineクラス」を new してしまっている private Engine engine = new Engine(); public void drive() { engine.start(); System.out.println("発進します"); } } |
このコードでは、Car クラスが Engine クラスの「作り方(new Engine())」を直接知ってしまっています。これが密結合です。
密結合の問題点
この「密結合」なコードには、大きく2つの問題があります。
問題点1:変更に弱い(柔軟性がない)
もし「高性能な SuperEngine クラスに差し替えたい」と思ったら、どうなるでしょうか?
|
1 2 3 4 5 |
public class SuperEngine { // 新しいエンジン public void start() { System.out.println("スーパーエンジン始動!"); } } |
Car クラスの private Engine engine = new Engine(); の部分を、private SuperEngine engine = new SuperEngine();(※型も変える必要あり)のように、Car クラスのコードを直接書き換える必要があります。
今は1箇所ですが、もし100個のクラスが Engine を使っていたら、100箇所すべてを修正しなければならず、バグの温床になります。
問題点2:テストがしにくい
Car クラスの drive メソッドだけをテスト(単体テスト)したい場合を考えます。 しかし、このコードでは Car を動かすと、必ず本物の Engine も一緒に動いてしまいます。
もし Engine クラスが、内部でDB接続やネットワーク通信など、重い処理をしていたらどうでしょう? Car クラスの簡単なテストをしたいだけなのに、DBの準備まで必要になってしまい、テストが非常に困難になります。
2. 疎結合(Loose Coupling)とは?
疎結合とは、クラス間の依存関係を弱め、お互いが「具体的な実装(中身)」ではなく、「共通のルール(インターフェース)」にだけ依存する状態を指します。
-
比喩: 「ネジの規格(部品Bのルール)さえ合っていれば、どのメーカーのネジでも使える家具(部品A)」。
良いコード例:『車』は『エンジンという規格』だけを知る
疎結合を実現する最も一般的な方法は、「インターフェース」を使うことです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// 1. まず「エンジンの共通ルール(規格)」を決める (インターフェース) public interface Engine { void start(); } // 2. 規格を実装した具体的なエンジンA public class NormalEngine implements Engine { @Override public void start() { System.out.println("ノーマルエンジン始動"); } } // 3. 規格を実装した具体的なエンジンB public class SuperEngine implements Engine { @Override public void start() { System.out.println("スーパーエンジン始動!"); } } |
そして、Car クラスを以下のように修正します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 車クラス(これが疎結合の例) public class Car { // どのエンジンが来るか分からないが、「Engineという規格」の部品が来る private final Engine engine; // 「コンストラクタ(設計図)」で、外部から完成品のエンジンを受け取る public Car(Engine engine) { this.engine = engine; } public void drive() { engine.start(); System.out.println("発進します"); } } |
疎結合のメリット
このコードの最大のポイントは、Car クラスが new を使わなくなったことです。 Car クラスは、もはや NormalEngine なのか SuperEngine なのかを知りません。「Engine という規格(start メソッド)さえ満たしていれば何でもOK」という状態になりました。
メリット1:変更に強い(柔軟性が高い)
Car を使う側(main メソッドなど)で、渡すエンジンを変えるだけです。
|
1 2 3 4 5 6 7 8 9 |
// Aパターン: ノーマルエンジンを渡す Engine normal = new NormalEngine(); Car carA = new Car(normal); carA.drive(); // Bパターン: スーパーエンジンを渡す Engine superE = new SuperEngine(); Car carB = new Car(superE); carB.drive(); |
Car クラスのコードを1行も変更することなく、エンジンを自由に差し替えられました。
メリット2:テストがしやすい
これが非常に重要です。テストの時は、「テスト用の偽物(モック)のエンジン」を渡すことができます。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// テストコードの例 @Test void carDriveTest() { // 1. テスト用の「偽物」のエンジン(MockEngine)を作る Engine mockEngine = new MockEngine(); // DB接続などしないダミー // 2. 偽物を渡してCarを作る Car testCar = new Car(mockEngine); // 3. これでDBなどに接続せず、Carクラスのロジックだけを安全にテストできる testCar.drive(); } |
3. そして、DI(依存性の注入)へ
おめでとうございます! これであなたも「疎結合」のメリットを理解できました。
しかし、ここで新たな疑問が生まれます。
「
Carクラスはnewしなくなったけど、結局、誰がnew Engine()して、誰がCarに渡して(注入して)くれるの?」
上の例(main メソッド)では私たちが手動で new して new Car(engine) を呼びました。 しかし、アプリケーションが巨大になり、1000個もの部品(クラス)がお互いを必要とし始めたら、この複雑な依存関係の「組み立て作業」をすべて手動で管理し続けるのは、実装上は可能であっても保守・運用上、現実的ではありません。コード量が膨大になり、どこかで間違いが発生するリスクが高まります。
そこで登場するのが、Springの「DIコンテナ」です。
DI(依存性の注入)とは、この「疎結合」を実現するために、 「依存関係(Engine)」を「外部(SpringのDIコンテナ)」が自動的に「注入(Car のコンストラクタに渡す)」してくれる仕組みのことです。
@Autowired というアノテーションは、まさに「Springさん、ここに適切な部品(Bean)を注入してください!」とお願いする目印だったのです。
では、次のセクションから、Springがどのようにこの「組み立て作業」を自動で行っているのか、その「魔法」の正体を詳しく見ていきましょう。
4.Springの「DIコンテナ」という巨大工場
疎結合を実現するために、「車(Car)は自分でエンジンを new しない」ところまでは理解できました。では、誰がエンジン(依存物)を作って、誰が Car に渡す(注入する)作業を担当するのでしょうか?
この「組み立て作業」をすべて引き受けてくれるのが、Springの核となる「DIコンテナ(または IoCコンテナ)」です。
DIコンテナの役割
DIコンテナは、アプリケーションが利用するすべての部品(クラスのインスタンス)を管理する、巨大な部品管理庫であり、組み立て工場です。
-
部品(Bean)の生成: アプリケーションで必要となるクラスのインスタンス(部品)をあらかじめ作っておきます。
-
部品の管理と注入: 部品をコンテナ内で管理し、誰かが「この部品(依存物)が欲しい」と言ったら、適切な部品を探し出して自動的に渡して(注入して)あげます。
Bean (ビーン) とは?
非常にシンプルです。「SpringのDIコンテナによって管理されているオブジェクト(インスタンス)」のことを指します。
5.@Autowired だけでインスタンスが使える仕組み
私たちがSpring Bootで書くコードは、このDIコンテナという「工場」に向けた「指示書」のようなものです。最も主要な指示書の一つが、@Autowired アノテーションです。
@Autowired は、「Springさん、このフィールド(またはコンストラクタ)に、適切なBeanを注入してください」とお願いする目印です。
この「魔法」がどのように動くのかを理解するために、まずはコード例を見てみましょう。
具体的なコード例:UserServiceとUserRepository
以下は、UserService が UserRepository の機能を使う、よくある依存関係の例です。
|
1 2 3 4 5 6 7 8 |
// 1. DBアクセス担当:リポジトリ層 @Repository // Springに「このクラスはBeanとして管理してね」と伝える public class UserRepository { public String findUserById(String id) { // 実際にはDBからデータを取得する処理 return "User Name (ID: " + id + ")"; } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 2. ビジネスロジック担当:サービス層 @Service // Springに「このクラスもBeanとして管理してね」と伝える public class UserService { // 【依存物】UserRepositoryのBeanが必要 private final UserRepository userRepository; // 【DI】コンストラクタでUserRepositoryを自動的に受け取る // ※@Autowired はコンストラクタが一つなら省略されます(推奨される方法) public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public String getUserInfo(String id) { // 自分でnewしていないuserRepositoryが使える! return "Fetched: " + userRepository.findUserById(id); } } |
上記のコードで、私たちが UserService のインスタンスを手動で new したり、UserRepository をセットしたりする作業は一切ありません。
このとき、裏側で Spring が実行している「魔法」の正体が、以下の4つのステップです。
ステップ1:スキャン(部品探し)
Spring Bootが起動すると、まず「コンポーネントスキャン」が始まります。Springは、パッケージ内にある特定のマーク(アノテーション)が付いたクラスを片っ端から探し出します。
-
対象となるアノテーション:
@Componentや、その仲間である@Service,@Repository,@Controller,@Configurationなどです。
ステップ2:Beanの生成と登録(部品棚へ)
Springは見つけたクラスを元にインスタンス(Bean)を生成し、DIコンテナという「部品棚」に登録します。
-
例:
@Repositoryが付いたUserRepositoryクラスのインスタンスが作られ、コンテナに保管されます。
ステップ3:依存関係の分析(組み立て図の確認)
次に、Springはステップ2で生成したすべてのBeanについて、「他の部品(依存物)を必要としていないか?」をチェックします。
@Service が付いた UserService のBeanを作ろうとしたとき、Springは UserService のコンストラクタやフィールドに @Autowired が付いているのを見つけます。
-
例: 「
UserServiceのコンストラクタにはUserRepositoryが必要だと書いてあるな」と認識します。
ステップ4:注入(部品の組み立てと完成)
Springは、ステップ3で必要だと判断された部品(例: UserRepository のBean)を、コンテナ内の部品棚から探し出します。
そして、その見つけ出したBeanを、自動的に UserService のコンストラクタ引数に渡してあげます(=注入)。
このプロセスを経ることで、私たちが UserService を利用する時には、すでに UserRepository がセットされた「完成品」の状態で手元に渡されるのです。
6.DIを使うことで得られる強力なメリット
SpringのDIコンテナの恩恵は、コードが短くなることだけではありません。「密結合」の問題を解決し、「疎結合」を達成することで、以下の決定的なメリットが生まれます。
| メリット | なぜDI(疎結合)で実現できるのか? |
| テスト容易性 | テストの際に、本物の依存物(DBアクセスなど)ではなく、テスト専用の「偽物(Mock)」をDIコンテナ経由で簡単に注入できるため、単体テストが非常に迅速かつ容易に行えます。 |
| 高い拡張性 | インターフェースに依存する設計になるため、主要なロジック(Car)のコードを一切変更せず、新しい依存物(SuperEngine)を追加するだけで機能拡張が可能です。 |
| 不変性の確保 | 特に「コンストラクタインジェクション」を使うことで、依存関係を final フィールドとして定義できるため、インスタンスが生成された後で意図せず中身が書き換わることを防げます。 |
7. まとめ
-
密結合は、変更に弱くテストしにくいコードを生み出す。
-
DI(依存性の注入)は、この問題を解決し、「疎結合」を実現するための設計手法。
-
Springの DIコンテナ は、アプリケーション起動時に
@Serviceなどが付いたクラスをスキャンして「Bean」として管理する。 -
@Autowiredは、コンテナに対して「ここに適切なBeanを注入せよ」と指示する目印である。 -
この仕組みのおかげで、私たちは「部品の組み立て」をSpringに任せ、本来のビジネスロジックに集中できる。
エコモットでは、Spring Bootをはじめとする最新技術を駆使して、
IoTに関連する多彩なプロジェクトに取り組んでいます。
私たちと一緒に、革新的なモノづくりに挑戦し、未来を創る仲間を募集しています。



