この記事では、DI(依存性注入)の意味からメリット、Spring Bootでの書き方までを分かりやすく整理し、テストしやすい設計の第一歩を踏み出します。

Dependency Injectionとは

Dependency Injection (DI) は、日本語だと「依存性の注入」(または「依存性注入」「依存関係の注入」)と訳されます。

ざっくり言うと「あるクラスが必要とする別のオブジェクト(依存先)を、自分で作らずに外から渡してもらう」考え方です。

たとえば、注文を処理する OrderService が、支払い処理の PaymentGateway を必要とするとします。この PaymentGateway依存 (dependency) で、外から渡すことが 注入 (injection) です。

「依存性」「注入」という言葉の意味

用語が固くて最初はピンと来にくいので、ここで噛み砕いておきます。

  • 依存 (dependency): あるクラスが動くために必要とする別のオブジェクトのこと。OrderServicePaymentGateway がないと注文を完了できないので、PaymentGatewayOrderService の依存です
  • 依存性 / 依存関係: どちらも英語の dependency の訳語で、実務ではほぼ同じ意味で使われます。「依存性の注入」も「依存関係の注入」も指しているものは同じです
  • 注入 (injection): その依存を、クラスの内部で new するのではなく、外側から渡してあげること。Spring Boot では IoC コンテナがこの「渡す」役を担います

Spring Framework と Spring Boot の DI の関係

「Spring の DI」と「Spring Boot の DI」は別物ではありません。Dependency Injection の本体は Spring Framework の IoC コンテナ(ApplicationContext で、Spring Boot はそのコンテナをそのまま使っています。Spring Boot が上乗せしているのは、@SpringBootApplication によるコンポーネントスキャンの自動化や、よく使うライブラリの Bean を自動登録する自動設定(Auto-configuration)の部分です。

そのため、Spring Framework の dependency injection の知識はそのまま Spring Boot でも通用しますし、この記事の内容も Spring Framework 単体で読み替えて問題ありません。コンテナに Bean を登録するための @Component / @Service / @Repository の使い分けは @Componentとは何か・@Beanとの違い で詳しく解説しています。

DIを使う5つのメリット

まず「DIを使うと何が嬉しいのか」を先にまとめておきます。

  • テストが書きやすくなる: 本物のDBや外部APIを呼ばずに、偽物(モック/フェイク)に差し替えてユニットテストできる
  • 実装を差し替えやすい: 支払い基盤を Stripe → PayPay に変えるときも、OrderService 側のコードを修正せずに済む
  • 依存関係が見える化される: コンストラクタ引数として依存が並ぶため、クラスが何に頼っているかが一目でわかる
  • 無駄なインスタンスが増えない: Spring の Singleton スコープによって、同じBeanはアプリ内で基本1つだけ使い回される
  • 責務分割が自然に進む: 「依存を外から渡す」前提で書くと、1クラスに詰め込みすぎる設計が抑制される

DIがないと何が困るのか

DIを使わないと、クラスの中で依存オブジェクトを直接 new しがちです。

public class OrderService {
    private final PaymentGateway paymentGateway = new StripePaymentGateway();

    public void checkout() {
        paymentGateway.pay();
    }
}

この書き方は一見シンプルですが、次のような問題が起きやすいです。

  • 支払い手段を変えたい(Stripe → PayPay など)ときに OrderService を直接修正する必要がある
  • テスト時に「本物の決済」を呼んでしまう危険がある
  • 依存が増えるほど、クラスが“何に依存しているか”が見えづらくなる
  • 同じ依存を使う場所が増えるほど、あちこちで new されて無駄なインスタンスが増えやすい

DIは、こうした「変更に弱い」「テストしにくい」状態を避けるための基本テクニックです。

依存を外から渡すと何がうれしいのか

DIを使うと、依存先を外から渡します。

public class OrderService {
    private final PaymentGateway paymentGateway;

    public OrderService(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }

    public void checkout() {
        paymentGateway.pay();
    }
}

これだけで設計がかなり良くなります。

  • OrderServicePaymentGateway の「具体的な実装」を知らなくていい
  • 本番では StripePaymentGateway、テストでは FakePaymentGateway を差し替えられる
  • 依存関係がコンストラクタ引数として見えるので、構造が読みやすくなる

この「具体ではなく抽象(インターフェース)に依存する」感覚は、DIとセットで身につけると強いです。

Spring Bootではインスタンスが無駄に増えにくい

ここ、まさにDI(というよりSpringのコンテナ管理)の大きなメリットです。

Spring Bootで @Component / @Service / @Repository などを付けて登録したクラスは、特に指定しない限り Singletonスコープ になります。つまり「アプリ起動中、同じBeanは基本1つだけ作られ、それが使い回される」ということです。

@Service
public class OrderService {
    public String status() {
        return "ok";
    }
}

@RestController
public class OrderController {
    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }
}

@RestController
public class AdminController {
    private final OrderService orderService;

    public AdminController(OrderService orderService) {
        this.orderService = orderService;
    }
}

この場合、OrderService は基本的に1回生成され、OrderControllerAdminController の両方に同じインスタンスが渡されます。

一方で、DIを使わず各所で new OrderService() してしまうと、呼び出し箇所の数だけインスタンスが増えます。重いオブジェクト(DB接続や外部APIクライアント、設定を大量に抱えるクラスなど)だと、起動時間やメモリの面でも地味に効いてきます。

例外もある

「いつでも必ず1個」というわけではなく、必要に応じてスコープは変えられます。たとえば prototype にすると、注入のたびに新しいインスタンスが作られます。

@Service
@Scope("prototype")
public class ReportBuilder {
}

なので、正確には「デフォルトがSingletonだから無駄に増えにくい」と覚えておくのがいいです。prototype / request / session など各スコープの挙動と使い分けは、Spring Boot の Bean スコープ完全ガイド にまとめています。

DIの主な注入方法

3つの注入方法の比較

どれを選ぶか迷ったときの早見表です。実務ではコンストラクタインジェクションを軸にするのが安全です。

注入方法推奨度テスト容易性final主な用途
コンストラクタインジェクション高い可能必須依存・本番コードの基本形
フィールドインジェクション低い不可既存コードの読解時に遭遇する程度
セッターインジェクション不可任意依存・テストでの差し替え

コンストラクタインジェクション

一番おすすめです。依存が必須であることが明確で、final にしやすく、テストもしやすいです。

@Service
public class OrderService {
    private final PaymentGateway paymentGateway;

    public OrderService(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }
}

フィールドインジェクション

こちらもよく使われている印象です。記述は楽ですが、依存がコンストラクタに現れないため関係が見えづらく、final にもできません。テスト時にはリフレクションで依存を差し込む必要も出てくるので、新規コードでは避けるのが無難です。

@Service
public class OrderService {
    @Autowired
    private PaymentGateway paymentGateway;
}

セッターインジェクション

依存が「任意」のときには使える選択肢ですが、必須依存には向きません。

@Service
public class OrderService {
    private PaymentGateway paymentGateway;

    @Autowired
    public void setPaymentGateway(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }
}

Spring BootでDIが動く仕組み

Spring Bootでは、IoCコンテナ(Springコンテナ)がオブジェクト生成と依存解決を担当します。

  • IoC (Inversion of Control) は「制御の反転」という意味で、オブジェクトを作る主導権がアプリ側からフレームワーク側へ移るイメージです
  • DIは、そのIoCを実現するための具体的な仕組みのひとつです

Spring Bootでよく使うのは、クラスにアノテーションを付けてコンテナに登録する方法です。

public interface PaymentGateway {
    void pay();
}

@Component
public class StripePaymentGateway implements PaymentGateway {
    @Override
    public void pay() {
        System.out.println("Pay with Stripe");
    }
}

@Service
public class OrderService {
    private final PaymentGateway paymentGateway;

    public OrderService(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }
}

この状態でアプリを起動すると、Springが次のことをやってくれます。

  • StripePaymentGateway を生成してコンテナに登録
  • OrderService を生成するときに PaymentGateway が必要なのを見て、自動的に注入
  • デフォルトでは、登録されたBeanを基本1つ作って使い回す(Singleton)

@Beanで依存を登録するパターン

@Configuration クラスと @Bean メソッドの基礎については @Configurationとは何か で詳しく解説しています。あわせて読むと、Bean 登録の全体像がつかめます。

外部ライブラリのクラスなど、アノテーションを付けられない場合は @Configuration@Bean を使います。

@Configuration
public class AppConfig {

    @Bean
    public PaymentGateway paymentGateway() {
        return new StripePaymentGateway();
    }
}

これで PaymentGateway もSpringコンテナ管理になり、他のクラスへ注入できます。

テストでDIのありがたみが一気にわかる

Spring Boot のテスト全般(@SpringBootTest / @WebMvcTest / Mockito の使い方)は JUnit と Mockito で始める Spring Boot 単体テスト入門 を、Spring Boot 3.4 以降で @MockBean の代わりに使う @MockitoBean については @MockBean から @MockitoBean への移行ガイド をあわせてどうぞ。

DIのメリットが一番大きいのはテストです。

たとえば、テストでは本物の決済を呼びたくないので、偽物を渡します。

class FakePaymentGateway implements PaymentGateway {
    boolean called = false;

    @Override
    public void pay() {
        called = true;
    }
}

@Test
void checkout_calls_payment() {
    FakePaymentGateway fake = new FakePaymentGateway();
    OrderService service = new OrderService(fake);

    service.checkout();

    assertTrue(fake.called);
}

「外から渡せる」だけで、テストが安全で速く、書きやすくなります。

初心者がつまずきやすいポイント

実装が複数あると注入先が決められない

@Component("stripe") のように指定している名前は Bean 名 と呼ばれます。命名のルールや @Qualifier との関係は Springの@Bean「名前」入門 で詳しく解説しています。

PaymentGateway の実装が2つ以上あると、Springはどれを注入すべきか判断できずエラーになります。そんなときは @Qualifier を使って指定します。

@Component("stripe")
public class StripePaymentGateway implements PaymentGateway { /* ... */ }

@Component("paypal")
public class PaypalPaymentGateway implements PaymentGateway { /* ... */ }

@Service
public class OrderService {
    private final PaymentGateway paymentGateway;

    public OrderService(@Qualifier("stripe") PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }
}

循環参照になって起動できない

A が B を必要とし、B が A を必要とする状態は 循環参照(circular reference) です。Spring Boot 2.6 以降はデフォルトで循環参照が禁止されており、起動時に次のようなエラーで止まります。

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  orderService defined in file [.../OrderService.class]
↑     ↓
|  inventoryService defined in file [.../InventoryService.class]
└─────┘

内部的には BeanCurrentlyInCreationException が投げられています。設計の匂いが強いサインなので、対処は次の順番で検討するのがおすすめです。

  1. 責務を分割する(本命): A と B の両方が使っている処理を第3のクラス C に切り出し、A → C、B → C という一方向の依存に直します。循環参照は「1クラスに責務を詰め込みすぎ」の結果であることがほとんどです
  2. イベントで依存方向を断ち切る: 「注文確定後に在庫を更新」のような処理は、ApplicationEvent で通知すれば、サービス同士が直接呼び合わずに済みます
  3. @Lazy で生成を遅延させる(応急処置): 片方のコンストラクタ引数に @Lazy を付けるとプロキシが注入され、起動は通ります。ただし設計上の問題は残るので、あくまで一時しのぎです

spring.main.allow-circular-references=true で禁止を解除することもできますが、根本解決にはならないので、新規コードでは使わないほうが安全です。

SingletonのBeanに状態を持たせてしまう

Bean がいつ生成・初期化・破棄されるか(@PostConstruct / @PreDestroy)を押さえておくと、この問題の理解が早くなります。詳しくは Spring Boot の Bean ライフサイクルと @PostConstruct / @PreDestroy を参照してください。

SpringのBeanはデフォルトでSingletonなので、フィールドに状態(例: カウンタや一時データ)を持つと、複数リクエストで共有されて思わぬバグになることがあります。

「サービスは基本的にステートレス(状態を持たない)にする」と覚えておくと安全です。状態が必要なら、メソッド内のローカル変数に閉じ込めるか、スコープの見直しを検討します。

まとめ

  • DI は「依存するオブジェクトを外から渡してもらう」設計
  • new で直接作るのをやめると、変更に強く、テストしやすくなる
  • Spring Bootでは IoCコンテナ が生成と注入を肩代わりしてくれる
  • デフォルトの Singleton により、同じクラスのインスタンスが無駄に増えにくい
  • 実務では基本的に コンストラクタインジェクション を軸にすると安定する

DIが腹落ちすると、Spring Bootのコードが一段読みやすくなります。次は「どこをインターフェースにすると差し替えやすいか」を意識すると、設計力がぐっと上がるはずです!

Spring Boot 3.x での現在のベストプラクティス

補足(Spring Boot 4.x でも同じ): ここで挙げる内容は Spring Boot 4.x / Spring Framework 7 でも変わりません。公式の説明は Spring Boot リファレンス: Spring Beans and Dependency Injection にまとまっています。@RequiredArgsConstructor を含む Lombok の導入手順と注意点は Spring Boot での Lombok の使い方 を参照してください。

最後に、現在(Spring Boot 3.x / Java 17+ 系)の慣行をいくつか補足しておきます。

  • @Autowired は省略するのが主流: Spring Framework 4.3 以降、コンストラクタが1つだけならアノテーションを書かなくても自動でインジェクションされます。
  • Lombok の @RequiredArgsConstructor と相性が良い: final フィールドだけ宣言しておけば、コンストラクタを Lombok が生成してくれるため、ボイラープレートが大幅に減ります。
  • record でのコンフィグバインドとの混同に注意: @ConfigurationProperties 用の record と、DI 対象の Bean は役割が違います。DI で渡したいのはサービスやリポジトリ、設定値の保持は record + @ConfigurationProperties という整理が安全です。
@Service
@RequiredArgsConstructor // Lombok がコンストラクタを生成
public class OrderService {
    private final PaymentGateway paymentGateway;
}

DI 後のサービス層の疎結合化をさらに進めたい場合は、Spring Boot の ApplicationEvent でモジュール間を疎結合にする方法 や、入力バリデーションを切り出す カスタムバリデーションアノテーションの作り方 と組み合わせると、設計の選択肢が広がります。