Spring BootでわかるDependency Injection入門
この記事では、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();
}
}
これだけで設計がかなり良くなります。
OrderServiceはPaymentGatewayの「具体的な実装」を知らなくていい- 本番では
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回生成され、OrderController と AdminController の両方に同じインスタンスが渡されます。
一方で、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のコードが一段読みやすくなります。次は「どこをインターフェースにすると差し替えやすいか」を意識すると、設計力がぐっと上がるはずです!