Spring Bootを学び始めると、早い段階で @Autowired や「DI」という言葉が出てきます。
ただ、最初は「自動で入れてくれる便利な仕組み」くらいの理解になりやすく、
DIとIoCの違いや、なぜコンストラクタインジェクションがよく使われるのかが曖昧なまま進みがちです。
このあたりが曖昧でも一応コードは書けます。
ですが、少し複雑な画面や業務処理を書くようになると、「なぜこのクラスで new してはいけないのか」
「なぜフィールドインジェクションが避けられがちなのか」でつまずきやすくなります。
Spring Frameworkでは、IoCコンテナがオブジェクトの生成や依存関係の注入を担当します。
DIは、そのコンテナから必要な依存オブジェクトを受け取って動く考え方です。
DI / IoCを最初にどう理解すればよいか
DIは「依存オブジェクトを受け取る」考え方
DIは Dependency Injection の略です。
日本語では「依存性の注入」と呼ばれます。
少し言い換えると、あるクラスが自分で必要なオブジェクトを作るのではなく、外から受け取って使う考え方です。
たとえば、注文処理を行うServiceがあるとします。
このServiceがRepositoryを使いたいとき、クラスの中で new OrderRepository() と作るのではなく、
外から渡してもらう形にします。
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}Spring FrameworkのDIでは、オブジェクトは自分が使う依存関係をコンストラクタ引数やプロパティとして定義し、コンテナがその依存関係を注入します。
最初は、必要な部品を自分で作るのではなく、受け取って使うのがDIと理解すれば十分です。
IoCは「生成や管理の主導権をSpringに渡す」考え方
IoCは Inversion of Control の略です。
日本語では「制御の反転」と呼ばれます。
言葉だけだと少し硬いですが、実際に言いたいことはそこまで難しくありません。
これまで自分でやっていたオブジェクト生成や組み立てを、Spring側に任せるという考え方です。
Spring Frameworkのドキュメントでも、IoCコンテナはBeanの作成、設定、
組み立てを担う存在として説明されています。
つまり、
- DIは依存オブジェクトを受け取る仕組み
- IoCはその仕組みをSpringコンテナが担う考え方
という関係で見ると整理しやすいです。
Spring BootではBeanとして管理される
Spring Bootでは、Springが管理するオブジェクトを Bean として扱います。@Service @Component @Repository などが付いたクラスは、Springの管理対象になり、DIの候補になります。
たとえば、次のようなServiceとRepositoryがあるとします。
@Repository
public class OrderRepository {
public void save() {
// 保存処理
}
}
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void execute() {
orderRepository.save();
}
}このとき、OrderRepository と OrderService はSpring管理のBeanとして扱われ、OrderService の生成時に OrderRepository が注入されます。
こうした依存関係の配線をSpringコンテナが行うのが、SpringにおけるDIの基本です。
@Autowired の基本
@Autowired で依存関係を注入できる
@Autowired は、SpringのDI機能で依存関係を注入したい場所を示すアノテーションです。
たとえば、コンストラクタに付けると次のようになります。
@Service
public class OrderService {
private final OrderRepository orderRepository;
@Autowired
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void execute() {
orderRepository.save();
}
}このコードでは、Springが OrderRepository を見つけて、
OrderService のコンストラクタ引数に渡してくれます。
ただ、最近のSpringではコンストラクタが1つだけなら @Autowired を省略できるケースが多いので、
実際のコードでは省略形を見ることもよくあります。
フィールドインジェクションよりコンストラクタインジェクションが基本
@Autowired はフィールドにも付けられます。
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
public void execute() {
orderRepository.save();
}
}書き方としては短いですが、今はこの形よりコンストラクタインジェクションが基本と考えることが多いです。
理由は、依存関係がコード上ではっきり見えるからです。
クラスに何が必要なのかがコンストラクタを見るだけでわかるので、保守しやすくなります。
Spring Framework自体もコンストラクタ注入を正式にサポートしていて、
依存関係をコンストラクタ引数で宣言する形を基本的なDI手段として扱っています。
Spring Bootではコンストラクタが1つなら @Autowired を省略できることがある
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void execute() {
orderRepository.save();
}
}コンストラクタが1つだけであれば、Springはそのコンストラクタを使って依存関係を解決できます。
そのため、@Autowired を書かなくても動く構成が一般的です。
Springの @Autowired ドキュメントでも、候補コンストラクタの扱いと自動解決のルールが説明されています。
初学者のうちは「@Autowired を付けないと注入できない」と思いがちですが、
コンストラクタが1つなら省略されることがあると知っておくと、サンプルコードを読むときに混乱しにくくなります。
コンストラクタインジェクションを使う理由
依存関係が明確になる
コンストラクタインジェクションの一番のメリットは、クラスが何に依存しているかが明確になることです。
@Service
public class PaymentService {
private final PaymentRepository paymentRepository;
private final MailService mailService;
public PaymentService(PaymentRepository paymentRepository,
MailService mailService) {
this.paymentRepository = paymentRepository;
this.mailService = mailService;
}
}PaymentService が PaymentRepository と MailService を必要としていることが一目でわかります。
一方で、フィールドインジェクションだと、フィールド定義を順番に追わないと依存関係が見えにくくなります。
クラスの責務が重くなっていることにも気づきやすいので、設計の見直しにもつながります。
テストしやすくなる
コンストラクタインジェクションは、テストしやすさの面でもかなり有利です。
OrderRepository mockRepository = new MockOrderRepository();
OrderService orderService = new OrderService(mockRepository);このように、テスト時に差し替えたい依存オブジェクトをコンストラクタから渡しやすくなります。
Springコンテナに頼らなくても、単体テストでクラスを組み立てやすいのは大きな利点です。
実務では「動くからOK」で流しがちですが、後からテストを書く場面ではこの差がかなり効きます。
必須依存を安全に扱いやすい
コンストラクタインジェクションでは、必要な依存関係がそろわないとオブジェクトを作れません。
そのため、必須の部品が欠けた不完全な状態のインスタンスを作りにくくなります。
Springの古いリファレンスでも、コンストラクタベースのDIはオブジェクトを完全に初期化された状態で、
扱いやすいという考え方が示されています。
入門段階では、このクラスに絶対必要なものはコンストラクタで受け取ると覚えておくとかなり整理しやすいです。
DIでハマりやすいポイント
Beanとして登録されていないと注入できない
当たり前に見えますが、ここはかなりよくハマります。
Springが注入してくれるのは、あくまでSpring管理のBeanです。
そのため、対象クラスに @Component @Service @Repository などが付いていない、
あるいは設定クラスで @Bean 登録していない場合は、注入できません。
public class MailService {
public void send() {
// メール送信
}
}
@Service
public class UserService {
private final MailService mailService;
public UserService(MailService mailService) {
this.mailService = mailService;
}
}このままだと、MailService はBeanとして登録されていないので注入対象になりません。
まずは、Springに管理してもらう対象かどうかを確認するクセを付けると、切り分けしやすくなります。
同じ型が複数あると注入先を絞る必要がある
同じインターフェースの実装が複数ある場合、Springはどれを注入すればよいか判断できないことがあります。
このときは @Primary や @Qualifier を使って候補を絞ります。
Spring Frameworkの @Primary Javadocでは、複数候補があるときに優先するBeanを指定できます。
たとえば、次のようなケースです。
public interface PaymentGateway {
void execute();
}
@Component
@Primary
public class CreditCardPaymentGateway implements PaymentGateway {
@Override
public void execute() {
// クレジットカード決済
}
}
@Component
public class BankTransferPaymentGateway implements PaymentGateway {
@Override
public void execute() {
// 銀行振込
}
}このように、複数候補があるなら「どれを使うか」まで設計する必要があります。
循環参照は設計の見直しが必要になる
もう1つよくあるのが循環参照です。
たとえば、AがBを必要として、BもAを必要としている状態です。
コンストラクタインジェクションでは、このパターンは解決できないことがあります。
Spring Frameworkの公式ドキュメントでも、コンストラクタ注入を中心に使っている場合、
解決不能な循環依存が発生すると BeanCurrentlyInCreationException が投げられると説明されています。
@Service
public class AService {
private final BService bService;
public AService(BService bService) {
this.bService = bService;
}
}
@Service
public class BService {
private final AService aService;
public BService(AService aService) {
this.aService = aService;
}
}このような場合、無理に注入方法で逃がすより、責務の切り分けが不自然ではないかを見直すほうが本質的です。
まとめ
Spring BootのDIは、必要なオブジェクトを自分で new するのではなく、Springから受け取って使う考え方です。
IoCは、その生成や組み立ての主導権をSpringコンテナに任せる考え方として理解すると整理しやすくなります。
最初に押さえたいポイントは次のとおりです。
- DIは依存オブジェクトを外から受け取る考え方
- Spring BootではBeanとして管理されたオブジェクトが注入される
@Autowiredは依存関係の注入に使える- 実務ではフィールドインジェクションよりコンストラクタインジェクションが基本
- Bean未登録 / 複数候補 / 循環参照はハマりやすい
このあたりが整理できると、Spring Bootのコードがかなり読みやすくなります。@Transactional や例外処理のようなSpringの機能も、Spring管理のBeanを前提に動いているので、
ここを理解しておく価値はかなり大きいです。


コメント