Spring BootでわかるDependency Injection入門


Last updated on

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

Dependency Injectionとは

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

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

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

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つだけ作られ、それが使い回される」ということです。

たとえば OrderService を2つのクラスで使うとしても、Springが同じインスタンスを注入してくれるので、無駄に複数生成されにくくなります。

@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 {
}

なので、正確には「Spring BootではデフォルトがSingletonだから、同じクラスのインスタンスが無駄に増えにくい」と覚えておくのがいいです。

DIの主な注入方法

DIにはいくつか注入方法がありますが、Spring Bootでは基本的に次の3つを見かけます。

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

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

@Service
public class OrderService {
    private final PaymentGateway paymentGateway;

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

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

こちらもよく使われている印象です。 記述が楽ですが、一方で依存が見えづらく、テストもしにくいので、基本は避けるのが無難かもしれません。

@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
public class AppConfig {

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

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

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

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);
}

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

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

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

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を必要とする状態は 循環参照 です。設計の匂いが強いので、責務の分割や依存方向の見直しを検討するとスッキリします。

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

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

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

まとめ

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

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