Skip to main content

SpringのDI(依存性注入)

この章では、Spring Frameworkが持つ DI(Dependency Injection: 依存性注入) の仕組みを学ぶ。 普段のJavaでは new を使って自分でインスタンスを作るが、Springでは「フレームワークが代わりにインスタンスを作り、必要な場所に渡してくれる」。 これにより、依存関係を意識せずに部品を組み合わせられるようになる。

DIの理解は、Springアプリケーション全体の構造を理解する上での前提となる。 この章を通じて、なぜコントローラやMyBatisのMapperが自分で new しなくても動くのか がわかるようになる。

学習のゴール

  • DI(依存性注入)の基本概念と目的を理解できる
  • Springがインスタンスを生成・管理・注入する仕組みを説明できる
  • @Controller / @Service / @Repository / @Component の役割を区別できる
  • コンストラクタインジェクション(@Autowired)で依存を受け取る方法を理解できる
  • 「自分で new するケース」と「DIに任せるケース」を区別できる
  • インターフェースとDIを組み合わせることで、コントローラを変更せずに実装を切り替えられる理由を説明できる
  • DIがアプリ全体の保守性・拡張性を高める仕組みを理解できる

コラム:なぜDIは理解が難しいのか?

DI(依存性注入)は初学者にとって非常に理解しづらい概念 である。

  • そもそも依存性注入という言葉自体が難解で、イメージしづらい(い、いぞんせい・・??ちゅうにゅう????)
  • DI以前の問題として、初学者にとって難しい interface の理解が前提知識として必要
  • DIは「設計」の話であり、コードの動作を直接変えるものではない
  • DIは「なぜそうするのか」の理由が分かりづらい
    • DIは テストコードを書く際に大きなメリット を発揮するが、初学者がテストコードを書く機会はほぼない
    • インターフェースを使った設計のありがたみ は、長年運用されていて仕様変更が多発するようなシステムでこそ実感できるが、初学者はそうしたプロジェクトに触れる機会が少ない

それでも、この章 (そして世のすべてのSpring教材) でDIを学ぶ理由は、「なぜSpringでは new しなくてもクラスが勝手に使えるのか」 を理解してもらうためである。 この理解があるかないかで、コントローラやMyBatisのコードを追うときの理解度が大きく変わる。


DIとは何か?

普段のJavaでは、必要なクラスを自分で new してインスタンスを作成する。

GreetingService service = new GreetingService();
String msg = service.getMessage();

小さなプログラムならこれで十分だが、アプリが大きくなると「どのクラスがどのクラスを new しているか」の管理が複雑になる。 さらに、クラスを差し替えたいときに、new を書いている場所をすべて修正しなければならない。

Springでは、この「インスタンスの生成と受け渡し」を DIコンテナ に任せられる。

  • 必要なクラスはSpringが自動でインスタンス化して管理する
  • 必要な場所に自動で「注入(Injection)」してくれる

これを 依存性注入(Dependency Injection, DI) と呼ぶ。

👉 自分で new しなくても、Springが必要なクラスを用意してくれる仕組み

このおかげで、コードの修正が少なく済み、複雑になっても整理しやすい。 初学者にとって最初は「ただの魔法」に見えるかもしれないが、 まずは 「DIとは使いたいクラスを宣言しておけばSpringが渡してくれる仕組みのこと」 と理解すればOK。


Springにインスタンス化を任せる仕組み(コンポーネントスキャン)

Springは起動時にプロジェクトをスキャンし、アノテーションの付いたクラスを自動でインスタンス化する。

  • @Controller : Web層(リクエストを受けるクラス)
  • @Service : ビジネスロジック層(処理を担当するクラス)
  • @Repository : データアクセス層(DBとやりとりするクラス)
  • @Component : 上記以外の汎用コンポーネント

👉 これらのアノテーションがインスタンスが「作られる側」の目印になる。 ※こうして生成され、Springのコンテナで管理されるインスタンスを Bean と呼ぶ。

Bean(ビーン)とは

Spring が管理するインスタンスのことを Bean と呼ぶ。 @Controller / @Service / @Repository / @Component のいずれかが付いたクラスは、 Spring 起動時に自動でインスタンス化されて Bean として登録される。

Bean は基本的に シングルトン(アプリ内に1つだけ存在)として管理される。 つまり、どのクラスから @Autowired で取得しても、同じインスタンスが渡される。

Springでは、インスタンスを「使う側」のクラスに @Autowired を付けることで、DIコンテナが自動的にインスタンスを渡してくれる

// 作られる側(サービス)
@Service
public class GreetingService {
public String getMessage() {
return "こんにちは!";
}
}
// 使う側(コントローラ)
@Controller
public class GreetingController {
private final GreetingService service;
@Autowired // SpringがGreetingServiceのBeanを注入
public GreetingController(GreetingService service) {
this.service = service;
}
@GetMapping("/greet")
public String greet(Model model) {
model.addAttribute("msg", service.getMessage());
return "greet";
}
}
  • @Service が付いた GreetingService は、Springが起動時にBeanとして自動登録する
  • GreetingController のコンストラクタに @Autowired を付けると、Springが GreetingService を見つけて自動的に渡してくれる
  • new GreetingService() を自分で書く必要はない

👉 言い換えると、「このクラスが必要」と宣言しておけば、Springが代わりに作って渡してくれる。 これがDIの基本的な仕組みであり、コードをシンプルに保ち、使う側が余計なことを考えずに済むようにしてくれる。

自分で new するケース / DIに任せるケース

Springを使っているからといって、すべてのインスタンスをDIに任せるわけではない。 「自分で new するもの」と「DIに任せるもの」を区別することが大切である。

List<String> tags = new ArrayList<>();
User user = new User("Taro", 20);

自分で new する(コンテナ管理の外でOK)

  • 一時的なオブジェクト 例: コレクション(ArrayListHashMap
  • DTO(データ転送オブジェクト) 単に値を保持するだけのクラス
  • 依存管理やライフサイクル管理が不要なもの その場で作って、その場で捨てるオブジェクト

👉 「その処理の中で完結するだけのインスタンス」は自分で new する。

DIに任せる(コンテナ管理に載せるべき)

  • アプリ全体で使う部品 コントローラ / サービス / リポジトリ など
  • 差し替えやテストが必要な対象 例: 実運用時は本物のDBに接続、テスト時はモックDBを注入
  • フレームワークと連携して管理が必要なもの DB接続、トランザクション、HTTPクライアント など

👉 @Controller / @Service / @Repository / @Component が付いたクラスは、Springが自動で管理する。 自分で new せずに、Springに任せておけばよい。

まとめ

  • 自分で new : その場かぎり、単純なデータ保持や計算処理に使うもの
  • DIに任せる : アプリ横断で利用される部品や、ライフサイクル管理が必要なもの

👉 簡単に言うと、 「すぐ捨てるものは自分で new」「アプリの部品はDIに任せる」 と覚えるとよい。


インターフェースで実装を差し替える(コントローラを触らずに切替可能)

ここでは、DIの最大のメリットのひとつを体感する。 それは「使う側(コントローラなど)を一切変更せずに、使われる側(サービスなど)の実装を差し替えられる」という点である。

  • 初学者にはまだ実感が湧きにくいが、実際のシステム開発では「処理内容を途中で切り替えたい」ケースは多い
    • 例:ある処理を行うプログラムがまだ完成していないため、一時的にモック(仮の処理)を使う
    • 例:仕様の大幅変更により、処理内容をまるごと差し替えたい
  • 通常Javaで new を使って依存クラスを直接呼び出していると、切り替え時に呼び出し元コードを書き換えなければならない
  • しかしDIとインターフェースを組み合わせれば、呼び出し元を一切触らずに差し替えられる
  • これが「拡張性」と「保守性」を高める大きなポイントである。
// 1) サービスのインターフェース
public interface GreetingService {
String getMessage();
}
// 2) GreetingServiceの実装A
@Service
@Primary // ← これがポイント①:複数実装があるときの優先指定
public class FormalGreetingService implements GreetingService {
@Override
public String getMessage() {
return "こんにちは。ご来訪ありがとうございます。";
}
}
// 3) GreetingServiceの実装B
@Service
public class CasualGreetingService implements GreetingService {
@Override
public String getMessage() {
return "やっほー!";
}
}
// 4) コントローラ(インターフェースにのみ依存)※このコードは変えない
@Controller
public class GreetingController {
private final GreetingService service; // ← 実装ではなく、インターフェースに依存
@Autowired
public GreetingController(GreetingService service) {
this.service = service;
}
@GetMapping("/greet")
public String greet(Model model) {
model.addAttribute("msg", service.getMessage());
return "greet";
}
}

どうやって切り替わるの?

  • Springは起動時に、GreetingService を実装したクラスを探す
  • 複数ある場合、@Primary が付いたインスタンスが 優先 される
  • 上記では FormalGreetingService@Primary を付けているため、フォーマルな挨拶文が使われる
  • @Primary を別の実装(例:CasualGreetingService)に付け替えれば、そのまま切り替わる

👉 コントローラのコードは一切変更しなくてよい。 これこそが「DIとインターフェース設計が組み合わさったときの威力」である。

ポイント

  • 依存するのは「実装」ではなく「インターフェース」
  • 切り替えはSpringのDIが担当するため、利用側コードに影響なし
  • 将来の拡張やテストが圧倒的に容易になる

よくある質問

Q. @Autowired は省略できますか?

A. コンストラクタが1つだけの場合、Spring 4.3以降では @Autowired を省略できる。 ただし、明示的に書いた方がコードの意図が伝わりやすいため、研修中は書くことを推奨する。


Q. フィールドインジェクション(@Autowired をフィールドに直接付ける)はダメですか?

A. 動作はするが、コンストラクタインジェクションの方が推奨される。

// フィールドインジェクション(非推奨)
@Controller
public class SampleController {
@Autowired
private SampleService service; // フィールドに直接
}

// コンストラクタインジェクション(推奨)
@Controller
public class SampleController {
private final SampleService service;

@Autowired
public SampleController(SampleService service) {
this.service = service; // final にできる、テストしやすい
}
}

コンストラクタインジェクションでは final にできるため、後から上書きされることがなく安全である。


Q. @Service@Component の違いは何ですか?

A. 動作上の違いはほとんどない。どちらもSpringのBeanとして登録される。 ただし、 @Service は「ビジネスロジックを担うクラス」であることを明示するための意味的な区別である。 コードの読みやすさのために、役割に合ったアノテーションを選ぶのが推奨される。


Q. @Repository@Service と何が違いますか?

A. @Repository はDB関連の例外を Spring の DataAccessException に変換する機能が付いている。 DBアクセスを担うクラスには @Repository を使うと、例外ハンドリングが統一される利点がある。


本章のまとめ

  • DI(依存性注入) とは、インスタンスを自分で new する代わりに、Springが作って渡してくれる仕組み
  • @Controller / @Service / @Repository / @Component を付けたクラスは、Springが起動時に探して自動でインスタンス化(Bean化)する
  • 使う側は コンストラクタインジェクション@Autowired)で受け取るのが基本
  • すべてを任せるわけではない:アプリ全体で使う部品はDI、一時的なデータ保持やコレクションなどは自分で new
  • インターフェースに依存 しておけば、@Primary の付け替えだけで実装を差し替えられる
    • 例:Repository層の実装がまだ完成していない間はモックを使い、完成したら本物の実装に切り替える
    • 例:仕様変更で処理内容をまるごと新しいクラスに差し替える
  • こうして「使う側を一切変えずに差し替え可能」にできるのがDIの大きなメリット
  • この仕組みのおかげで、後のMyBatisなどでも「コードに new がないのにインスタンスが用意されている」ように見える
  • 次章では、DIの仕組みを活かして MyBatisでDBにアクセスする方法 を学ぶ

DI(依存性注入)の説明として、最も適切なものを選べ。

正解

C. クラスが必要とする依存オブジェクトを自分でnewせずに外部(DIコンテナ)から注入してもらう設計パターンのこと

解説

DI(Dependency Injection:依存性注入)とは、クラスが必要とする他のオブジェクト(依存オブジェクト)を、自分で new して生成するのではなく、外部のDIコンテナが生成して注入する設計パターンである。

DIのメリット:

  • 疎結合: クラス間の依存関係が緩くなり、変更の影響が局所化される
  • テストしやすい: モック(テスト用オブジェクト)に差し替えやすい
  • 再利用性: 同一のBeanを複数箇所で再利用できる

SpringはDIコンテナとして機能し、@Autowired などを使って依存関係を自動解決する。

メッセージを返すサービスクラスを作成せよ。

public class MessageService { public String getMessage() { return "Hello from Service!"; } }

解答例
@Service public class MessageService { public String getMessage() { return "Hello from Service!"; } }
解説

@Service アノテーションを付けることでSpringがそのクラスをサービス層のBeanとして認識し、DIコンテナに登録する。

各ステレオタイプアノテーション:

  • @Controller: Webリクエストを処理するコントローラ層
  • @Service: ビジネスロジックを担当するサービス層
  • @Repository: データアクセスを担当するリポジトリ層
  • @Component: どの層にも分類できない汎用コンポーネント

これらはすべて @Component の派生アノテーションであり、コンポーネントスキャンの対象になる。

MessageService をコンストラクタインジェクションで受け取るコントローラを完成させよ。

@Controller public class MessageController {
public MessageController(MessageService messageService) { this.messageService = messageService; } }

解答例
private final MessageService messageService; @Autowired public MessageController(MessageService messageService) { this.messageService = messageService; }
解説

コンストラクタインジェクションは最も推奨されるDIの方式である。

@Autowired をコンストラクタに付けると、Springが適切なBeanをコンストラクタの引数に自動注入する。

final フィールドにすることで、注入後に値が変更されないことを保証できる。

Spring 4.3以降、コンストラクタが1つだけの場合は @Autowired を省略できる。

@Autowired アノテーションの仕組みとして、正しいものを選べ。

正解

B. `@Autowired` が付いた箇所に、DIコンテナが管理している同型のBeanを自動的に注入する

解説

@Autowired が付いた箇所に、SpringのDIコンテナが同じ型のBeanを自動的に注入する。

注入の方式:

  1. コンストラクタインジェクション(推奨): コンストラクタに @Autowired を付ける
  2. フィールドインジェクション: フィールドに直接 @Autowired を付ける(テストしにくいため非推奨)
  3. セッターインジェクション: セッターメソッドに @Autowired を付ける

同型のBeanが複数ある場合は @Primary@Qualifier で注入するBeanを指定できる。

@Component@Controller@Service@Repository の関係として、正しいものを選べ。

正解

D. `@Controller`、`@Service`、`@Repository` はすべて `@Component` を継承しており、役割を明示するための意味的な区別がある

解説

@Controller@Service@Repository はいずれも @Component のメタアノテーションを持つ派生アノテーションである。

機能的にはどれもDIコンテナへのBean登録という点では同じだが、以下の違いがある:

  • 役割が明確になり、コードの可読性が上がる
  • @Repository はデータアクセス例外の自動変換が有効になる
  • Spring AOPなどでアノテーションを使ったポイントカット指定ができる

一般的には役割に応じた正しいアノテーションを選ぶことが推奨される。

以下のコードのうち、SpringのDIに任せるべきものを正しく選んでいるのはどれか。

List<String> names = new ArrayList<>();
User user = new User("Taro", 20);
UserService userService = new UserService();
EmailSender emailSender = new EmailSender();

正解

A. `UserService` と `EmailSender` はDIに任せ、`List<String>` と `User` は `new` で生成する

解説

new とDIの使い分けの判断基準:

DIに任せるもの(Springが管理するもの):

  • サービスクラス(UserServiceEmailSenderなど)
  • リポジトリクラス
  • アプリ全体で共有するコンポーネント → ライフサイクル管理や依存関係解決をSpringに任せることで保守性が高まる

new するもの:

  • 一時的なデータ(new User("Taro", 20)new ArrayList<>()
  • 特定の処理内だけで使うローカルなオブジェクト → 毎回新しいインスタンスが必要なものは自分で new する

GreetingService インターフェースの実装クラス FormalGreetingService を優先的に注入されるBeanとして設定せよ。

@Service
public class FormalGreetingService implements GreetingService { public String getMessage() { return "こんにちは。"; } }

解答例
@Service @Primary public class FormalGreetingService implements GreetingService { public String getMessage() { return "こんにちは。"; } }
解説

同じインターフェースを実装したBeanが複数ある場合、Springはどれを注入するか迷う。

@Primary を付けると、そのBeanが優先的に注入される。

@Primary の代わりに @Qualifier("beanName") で名前を指定することもできる。

@Autowired
@Qualifier("casualGreetingService")
private GreetingService greetingService; // CasualGreetingServiceが注入される

インターフェース GreetingService を使って、コントローラが実装クラスに直接依存しないDI構成を完成させよ。

// コントローラはインターフェースに依存する @Controller public class GreetingController { private final
service; @Autowired public GreetingController(
service) { this.service = service; } }

解答例
public interface GreetingService { String getMessage(); } @Service @Primary public class FormalGreetingService implements GreetingService { ... } @Controller public class GreetingController { private final GreetingService service; @Autowired public GreetingController(GreetingService service) { this.service = service; } }
解説

インターフェースを使ったDI構成のメリット:

  1. コントローラはインターフェース(GreetingService)にのみ依存する
  2. 具体的な実装(FormalGreetingServiceCasualGreetingService)は交換可能
  3. テスト時にモック実装に差し替えやすい

これを「依存関係逆転の原則(DIP)」と呼び、保守性・拡張性が高い設計である。