(前回記事「なぜSpring BootはDI(依存性の注入)を使うのか?」の続きです)
前回は、密結合・疎結合の違いから始まり、DI(依存性注入)がなぜ必要なのか、そしてSpringのDIコンテナ(IoCコンテナ)がどのように依存関係を自動で解決してくれるのかを解説しました。
今回は、その「DIコンテナ」のさらに奥にある設計思想――IoC(Inversion of Control/制御の逆転)について、できるだけやさしく見ていきます。
実は、DIはIoCを実現するための手段のひとつです。ざっくり言うと、DIは「必要な部品を外から渡す仕組み」、IoCは「オブジェクトの生成やフレームワークからの呼び出しの主導権をフレームワークに渡す考え方」です。SpringのDIコンテナは「IoCコンテナ」とも呼ばれます。
ただし、IoCが逆転させるのは「オブジェクトの作り方」だけではありません。 コンテナが管理するBeanでは、生成・初期化・破棄をフレームワークに任せられます。また、イベントやWebリクエスト、スケジュール実行などのフレームワークの入口では、処理を呼び出すタイミングもフレームワークが管理します。
そもそもIoCが生まれた背景
フレームワークが今ほど発達する前、エンジニアは「全部自分でやる」のが当たり前でした。
- オブジェクトの生成は自分で
newする - 初期化の順序は自分で管理する
- 使い終わったリソースの解放も自分で書く
- 処理の呼び出しタイミングも自分で決める
シンプルなアプリなら問題ありません。しかしアプリの規模が大きくなると、この「全部自分で管理する」スタイルに限界が来ます。
たとえば20個のサービスクラスがあり、それぞれが3〜5個の依存関係を持っていたら──。 初期化順序の整合性を保ち、正しく配線するだけで、本来のロジックとは無関係な膨大な作業が発生します。
ここで「制御(コントロール)を自分ではなくフレームワークに任せよう」という発想が生まれました。これがIoCです。
IoC (Inversion of Control) :「自分で管理しない」という設計思想
IoCを一言でいうと、Hollywood Principle(ハリウッドの原則)です。
“Don’t call us, we’ll call you.” (君から電話してくるな。必要なときにこちらから呼ぶから。)
このシンプルな原則が、フレームワークがどのように「制御」を握るのか、その全体像を表しています。
ここからは、DIの背景にあるIoCの本質や、DI以外にもIoCが活きる場面を、初心者向けに順番に整理していきます。
※本記事ではSpring Frameworkを例に説明しますが、.NETやAngular、NestJSなど他の多くのIoC対応フレームワークでも同様の考え方・仕組みが使われています。
IoCが管理する世界 ── DI の具体例から見える全体像
IoC以前:自分が全部やる
以下は概念を示すために簡略化したコードです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
public class UserRepository { private DataSource ds; public UserRepository() { // 1. 自分で作る(生成) this.ds = new DataSource("jdbc:mysql://localhost:3306/mydb"); this.ds.init(); // 2. 自分で初期化する } } public class UserService { private UserRepository repo; public UserService() { // 3. 自分で組み立てて配線する(注入) this.repo = new UserRepository(); } public void registerUser(String name) { // 4. 自分で呼ぶ(実行) repo.save(name); } } public class App { public static void main(String[] args) { UserService service = new UserService(); service.registerUser("tanaka"); // 5. 自分で片付ける(破棄) // ← 実装するのが複雑で、漏れやすい } } |
生成→初期化→実行→破棄、すべてのライフサイクル管理が各クラスに分散し、制御が複雑になっています。
IoC採用後:Springが制御を代行
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Service public class UserService { private final UserRepository repo; // 「これが欲しい」とコンストラクタで宣言するだけ public UserService(UserRepository repo) { this.repo = repo; // Springが必要なものを判断して渡してくれる } public void registerUser(String name) { repo.save(name); // 自分は「呼ばれた時の業務ロジック」だけを書く } } |
UserService は誰が自分を作るのか、いつ破棄されるのかを知りません。Springが管理するのは、Beanの生成・依存関係の解決・ライフサイクルです。一方、registerUser() のような通常のメソッドは、コントローラーや別のサービスなど、呼び出し元が明示的に実行します。
IoCが管理するもの ── DIの先にある世界
DIでカバーされるのは主に「生成」と「注入」です。IoCはそれに加えて、以下の要素も強力に管理します。
① ライフサイクル管理
Springは、コンテナが管理するBeanをいつ初期化し、いつ破棄するかを制御します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Service public class DataSourceManager { @PostConstruct public void init() { // インスタンス生成後、依存関係の注入が完了したタイミングでSpringが自動実行 System.out.println("コネクションプールを初期化"); } @PreDestroy public void cleanup() { // ApplicationContextの終了時にSpringが自動実行 System.out.println("コネクションプールを解放"); } } |
ここでの説明は、主にデフォルトのsingleton Beanを想定しています。prototype Beanは生成後の破棄をコンテナが自動管理しないため、必要に応じて利用側で後始末を行います。
② イベントドリブンな実行
|
1 2 3 4 5 6 7 8 9 |
@Component public class OrderEventListener { @EventListener public void onOrderPlaced(OrderPlacedEvent event) { // アプリケーションが発行したイベントをSpringが受け取り、自動で呼び出す System.out.println("注文イベント受信: " + event.getOrderId()); } } |
@EventListener だけで注文を自動検知するわけではありません。注文処理側が ApplicationEventPublisher などを使い、OrderPlacedEvent を発行すると、Springが対応するリスナーを呼び出します。
③ スケジュール実行
タイマー管理やスレッド制御という複雑な「制御」もフレームワークに委ねます。
スケジュール実行を使うには、設定クラスなどで @EnableScheduling を有効にします。
|
1 2 3 4 |
@Configuration @EnableScheduling public class SchedulingConfig { } |
|
1 2 3 4 5 6 7 8 9 |
@Component public class ReportJob { @Scheduled(cron = "0 0 9 * * *") public void generateDailyReport() { // 毎朝9時になったらSpringが自動実行。ループ処理などを自分で書く必要はない System.out.println("日次レポートを生成"); } } |
④ スコープ管理 ── 「何個作るか」を宣言する
オブジェクトを「毎回新しく作るのか」「同じものを使い回すのか」は、開発者がスコープとして宣言します。Springはその宣言に従ってインスタンスを提供します。
| スコープ | 動作 | 使いどころ |
|---|---|---|
| singleton(デフォルト) | 1つのApplicationContext内で1つのインスタンスを使い回す | まずはこれを覚えれば十分 |
| prototype | 取り出すたびに新しいインスタンスを作る | 使い捨ての一時オブジェクト |
| request | HTTPリクエストごとに1つ作る | Webリクエスト固有の情報保持 |
prototypeスコープの注意点 ── 「宣言」と「取得」を分ける
prototypeスコープは「毎回新しく作る」設定ですが、singleton の bean にそのまま注入すると、最初の1回だけしか取り出されません。毎回新しいインスタンスがほしいときは、必要なタイミングで取りにいく形にします。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
@Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class TempFileWriter { public void write(String orderId) { } } // ❌ 注入した時点で1回だけ作られ、その後は同じものが使われる @Service public class OrderService { private final TempFileWriter writer; public OrderService(TempFileWriter writer) { this.writer = writer; // 以降ずっと同じインスタンス } } // ✅ 必要なときに取りにいく──毎回新しいインスタンス @Service public class OrderService { private final ObjectProvider<TempFileWriter> writerProvider; public OrderService(ObjectProvider<TempFileWriter> writerProvider) { this.writerProvider = writerProvider; } public void processOrder(String orderId) { TempFileWriter writer = writerProvider.getObject(); writer.write(orderId); } } |
IoCがないとどうなるか ── 実務での痛み
IoCを使わず、すべての依存関係を自分で解決しようとすると、main 関数は「巨大な配線図」と化します。
【Before】IoCがない世界:配線だけで力尽きる
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class App { public static void main(String[] args) { // 1. 膨大な初期化と配線コード(=制御を自分が持っている) Config config = new Config("app.properties"); DataSource ds = new DataSource(config.getDbUrl()); ds.init(); MailSender mail = new MailSender(config.getSmtpHost()); FileStorage storage = new FileStorage(config.getStoragePath()); UserRepository userRepo = new UserRepository(ds); OrderRepository orderRepo = new OrderRepository(ds); NotificationService notification = new NotificationService(mail); UserService userService = new UserService(userRepo, notification); // 2. 依存の順序を間違えると動かない OrderService orderService = new OrderService(orderRepo, userService, storage); // 3. ようやく本題(業務ロジック) orderService.placeOrder("12345"); // 4. 後片付けも手動 ds.close(); } } |
新しい部品(クラス)を追加するたびに、この main 関数の「配線図」を書き換え、依存順序に気を配る必要があります。
【After】IoCがある世界:依存関係を宣言する
IoCを採用すると、各クラスはコンストラクタで「自分はこれが欲しい」と宣言するだけになります。配線の詳細は、コンポーネントスキャンや @Bean 定義などの設定側に分離されます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@Service public class OrderService { private final OrderRepository orderRepo; private final UserService userService; private final FileStorage storage; // 「これが欲しい」とコンストラクタで宣言するだけ。 // 具体的に誰がどう作るかは、Spring(IoCコンテナ)にお任せ。 public OrderService(OrderRepository orderRepo, UserService userService, FileStorage storage) { this.orderRepo = orderRepo; this.userService = userService; this.storage = storage; } public void placeOrder(String orderId) { // インスタンス化や初期化を意識せず、すぐに業務ロジックに集中できる orderRepo.save(orderId); userService.notifyUser(orderId); } } |
この比較からわかる通り、IoCを導入すると以下のメリットがあります:
- 配線の詳細を分離できる:main 関数などで
newを繰り返す必要がなくなります - 依存順序からの解放:「Aを作る前にBを作らなきゃ…」といった順序管理をSpringが自動で行います
- 責務の集中:エンジニアは「オブジェクトの組み立て」という作業から解放され、本来の目的である「業務ロジック」の記述に集中できるようになります
まとめ:IoCが逆転させる3つの制御
| 制御の対象 | 従来のコード(主導:自分) | IoC(主導:フレームワーク) |
|---|---|---|
| 生成・注入 | 自分で new して渡す |
コンテナが作り、自動で注入する |
| 実行タイミング | 自分でメソッドを呼ぶ | イベントやスケジュールなどのフレームワーク入口で呼ばれる |
| ライフサイクル | 自分で初期化・破棄を書く | アノテーションに従い自動で管理される |
DIはIoCという巨大な氷山の一角に過ぎません。
「制御の流れそのものを明け渡す」ことで得られる柔軟性と保守性こそが、IoCの真価です。手放した主導権の分だけ、あなたのコードはシンプルで読みやすくなるはずです。
エコモットでは、Spring Bootをはじめとする最新技術を駆使して、
IoTに関連する多彩なプロジェクトに取り組んでいます。
私たちと一緒に、革新的なモノづくりに挑戦し、未来を創る仲間を募集しています。


