DIは何を解決しているのか ― newを減らすだけではないSpringの役割
@Serviceを付け、コンストラクタへRepositoryを書いたら動いた。最初はそれで十分ですが、「Springがよしなにしてくれる」で止めると、テストで差し替えたいときやBeanが見つからないときに困ります。
DI(Dependency Injection、依存性の注入)は、newを禁止する仕組みではありません。あるクラスが仕事に必要な相手を、自分で固定せず外から受け取る設計です。
注文受付を3つの箱へ分ける
- Controller:HTTPメッセージを受け取る窓口
- Service:注文受付の手順をまとめる箱
- Repository:DBへ保存・検索を依頼する窓口
ServiceがRepositoryの具体的な作り方まで知ると、業務の手順とDB接続の都合が一つのクラスへ混ざります。そこでServiceは「注文を保存できる相手」を必要とする、とだけ宣言します。
@Service
public class OrderApplicationService {
private final OrderRepository orderRepository;
public OrderApplicationService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public Order register(String productCode, int quantity) {
Order order = new Order(productCode, quantity);
return orderRepository.save(order);
}
}
@Serviceを付けたあとに起きること
@Serviceは「ここに業務処理があります」というコメントだけではありません。起動時のコンポーネント探索でクラスが見つかると、SpringはインスタンスをBeanとしてApplicationContextへ登録します。
そのうえで、コンストラクタが要求するOrderRepository型のBeanを探し、OrderApplicationServiceへ渡します。コンストラクタが一つなら、@Autowiredを省略できます。
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderApplicationService service;
public OrderController(OrderApplicationService service) {
this.service = service;
}
}
ControllerもServiceの生成方法を知りません。必要な型だけを示し、Springが管理するBeanを受け取ります。
newが悪いのではなく、固定する場所が問題
値オブジェクトやDTOをnewすることまで避ける必要はありません。外部資源へ接続するRepositoryや、複数の場所から使うServiceを呼び出し側で直接生成すると、実装の交換とテストが難しくなります。
OrderRepository repository = new InMemoryOrderRepository();
OrderApplicationService service = new OrderApplicationService(repository);
これはSpringを起動しない単体テストの組み立てです。DIを採用した結果、普通のJavaコードとして確認しやすくなっています。
DIの目的は、Springに任せること自体ではありません
クラスが必要とする相手を明示し、生成と利用を分離することが目的です。Springのコンテナは、その組み立てを担当します。
DTOは箱同士で運ぶメッセージ
DTOは、ControllerからServiceへ、またはServiceからControllerへ情報を渡す入れ物です。DBのテーブルをそのまま外へ見せるための型ではありません。
public record CreateOrderCommand(String productCode, int quantity) {}
Serviceへ文字列と整数をばらばらに渡す代わりに、注文受付というメッセージへまとめられます。DTOとEntityの違いは第5回で詳しく扱います。
初心者が確認する3点
- 起動クラスの探索範囲に
@Serviceが入っているか - 同じ型のBeanが複数あり、どれを注入するか曖昧になっていないか
- Controller、Service、Repositoryが循環して呼び合っていないか
Beanの全スコープやコンテナ内部APIは、この回では扱いません。まずはコンストラクタに「このクラスが必要とする相手」が並ぶ状態を作ります。
Spring Boot 3.5.16の検証プロジェクトでは、ApplicationContextがController、Service、Spring Data Repositoryを配線し、Springを起動しないServiceの単体テストも同じコンストラクタから組み立てられることを確認しました。
Beanが複数あるときは、曖昧さを隠さない
同じインターフェースを実装するBeanが二つあると、型だけでは選べません。
public interface MessageSender {
void send(String message);
}
@Service("mailSender")
class MailMessageSender implements MessageSender { /* ... */ }
@Service("chatSender")
class ChatMessageSender implements MessageSender { /* ... */ }
既定を一つ決めるなら@Primary、利用側が選ぶ理由を持つなら@Qualifier("mailSender")を使えます。ただし、呼び出し側の多くがBean名を知り始めたら、責務の分け方を見直す合図です。DIコンテナの解決方法で設計上の曖昧さを覆わないようにします。
実務ではこうなる
循環参照は、単なる設定ミスではなく責務の循環であることが多いです。OrderServiceがNotificationServiceを呼び、後者が再び前者を呼ぶなら、イベントや第三の調整役へ分けられないかを先に考えます。
テストのためにすべてをインターフェース化する必要もありません。外部I/O、時刻、乱数、別システムのように差し替える価値がある境界から始めます。小さな値オブジェクトやDTOは普通にnewし、共有する部品の生成と配線をApplicationContextへ寄せる。この線引きなら、クラス数だけを増やさずにDIの利点を得られます。
公式リファレンス
- Spring Framework Reference: Dependencies and Configuration
- Spring Framework Reference: Annotation-based Container Configuration
- Spring Framework Reference: Classpath Scanning
参考文献・参照資料
- 田村達也『後悔しないためのSpring Boot入門書:Spring解体新書(第2版)』:DIの章を参照。
- Mark Heckler, Spring Boot:立ち上げて実行する、ISBN 9798341626911(AI翻訳版):第1章・第2章のSpring Bootの構成要素を補助参照。
- 株式会社NTTデータ『Spring徹底入門 第2版』:第2章のDIコンテナとAOPの体系整理を参照。
書籍のコード・図・訳文は転載せず、注文受付の例へ置き換えています。アノテーションの現在の挙動はSpring公式資料を優先しました。
次回は、同じ設定キーが複数の場所にあるとき、どの値が採用されるかを追います。
