C
Spring Boot/核心概念/Lesson 04

IoC · DI · Bean — Springの*心臓部*

45分·theory

IoC · DI · Bean — Springの*心臓部*

🎯 このレッスンを読み終えたら

このレッスンを読み終えると、以下の3つを自信を持って実践できるようになります。

  • ✅ @Autowired が どのように Bean を注入するか(3つの方法)
  • ✅ 同じ型の Bean が競合したときの @Qualifier / @Primary による解決
  • ✅ @Configuration + @Bean による外部ライブラリの登録

学習目標をチェックリストとして手元に置き、すべてに答えられるようになったらレッスンを閉じましょう。

IoC(制御の逆転)— *自分で作らず、受け取る*

核心を一言で

IoC(Inversion of Control) = オブジェクトを 自分で直接作るのではなく、フレームワークが作って 自分に渡してくれる 仕組み。制御の流れが逆転した という意味。

昔のやり方 — すべてを 直接 作る

java
public class OrderService {
    private UserRepository userRepo = new UserRepository();     // 直接生成
    private EmailService email = new EmailService();             // 直接生成

    public void order(Long userId) {
        User u = userRepo.findById(userId);
        email.send(u.email(), "注文完了");
    }
}

一見すっきりして見えますが、大きな問題 があります:

  • テストが困難: OrderService だけテストしたいのに、本物の EmailService も動いてしまう — メールが実際に送信される
  • 変更が困難: EmailServiceSmsService に変わると、すべての呼び出しコード を修正しなければならない
  • 依存関係の追跡が困難: どこで誰が誰を作っているか 追跡不可能

新しいやり方 — 受け取って使う

java
@Service
public class OrderService {
    private final UserRepository userRepo;
    private final EmailService email;

    public OrderService(UserRepository userRepo, EmailService email) {
        this.userRepo = userRepo;        // Spring が注入
        this.email = email;
    }

    public void order(Long userId) {
        User u = userRepo.findById(userId);
        email.send(u.email(), "注文完了");
    }
}

OrderServiceどこで作られたかを気にしない。ただ 受け取って使う だけです。誰が作るかは Spring の責任

これが IoC。オブジェクト生成の制御権をフレームワークに委ねること

なぜ良いのか

1. テストが簡単

java
@Test
void 注文_テスト() {
    EmailService fakeEmail = new MockEmailService();   // 偽物
    OrderService svc = new OrderService(userRepo, fakeEmail);
    svc.order(42L);
    // fakeEmail 呼び出し有無検証 — 実際のメールは送信されない
}

2. 実装の差し替えが簡単EmailServiceKakaoMessageService に変えるには、Spring の設定を一行 変えるだけ。呼び出しコードはそのまま。

3. 依存関係が明確:コンストラクタのシグネチャを見るだけで 何が必要か 一目でわかる。

IoC コンテナ — Springの工場

オブジェクトを 作り・保管し・繋げる Springの核心コンポーネントを IoC コンテナ または ApplicationContext と呼びます。

動作の流れ:
1. アプリ起動時に Spring が @Component@Service@Repository@Controller が付いたクラスを すべてスキャン
2. 発見した各クラスの インスタンスを1つ作って コンテナに保管
3. 他の場所でそのオブジェクトが必要になったら、コンストラクタ・@Autowired注入

> 💡 コンテナに保管されたオブジェクトを Bean と呼びます。小さな単位のオブジェクトの集まり、という意味の比喩です。

まとめ

  • IoC = オブジェクト生成を フレームワークに委譲
  • メリット = テスト・差し替え・追跡がすべて容易になる
  • Spring コンテナ = Bean を作り接続する工場

これが Springの 最も根本的な思想。DI・AOP・トランザクションはすべて IoC の上で動作します。

DI(依存性の注入)— *3つの方法*

DI と IoC の関係

IoC のほうが大きな概念 であり、DI(Dependency Injection) はその 実現手段 のひとつです。オブジェクトが 必要とする依存性を外部から注入する 具体的な手法。

3つの注入方法

1. コンストラクタ注入(最も推奨)

java
@Service
public class OrderService {
    private final UserRepository userRepo;

    public OrderService(UserRepository userRepo) {
        this.userRepo = userRepo;
    }
}

メリット

  • final が使える → 不変性の保証
  • オブジェクト生成時点で すべての依存性が保証される
  • 循環参照が コンパイル時 に発見される
  • テスト時にモックオブジェクトを注入しやすい

Spring 4.3+ 以降、@Autowired を省略可能。Lombok の @RequiredArgsConstructor と組み合わせるとさらに簡潔になります:

java
@Service
@RequiredArgsConstructor
public class OrderService {
    private final UserRepository userRepo;
    private final EmailService email;
    // コンストラクタ自動生成
}

2. セッター注入

java
@Service
public class OrderService {
    private UserRepository userRepo;

    @Autowired
    public void setUserRepo(UserRepository userRepo) {
        this.userRepo = userRepo;
    }
}

メリット任意依存性(あってもなくてもよい)を表現できる
デメリットfinal が使えない。セッターが呼ばれなければ null になる。ほとんど使われない

3. フィールド注入(非推奨)

java
@Service
public class OrderService {
    @Autowired private UserRepository userRepo;       // ❌
}

一見簡潔に見えますが

  • テストが困難(Reflection 経由でしか注入できない)
  • 循環参照が 実行時 まで気づかない
  • 依存性が 追加しやすすぎて 無秩序に増える

> 💡 現場の合意常にコンストラクタ注入。フィールド注入は レガシーコード でのみ見られる。

同じ型の Bean が複数ある場合

java
@Service public class EmailService implements MessageService { }
@Service public class SmsService   implements MessageService { }

@Service
public class OrderService {
    public OrderService(MessageService msg) { }   // ❌ あいまい
}

Spring は どれを注入すればよいか わかりません。3つの解決策:

1. @Primary — デフォルトを指定:

java
@Service
@Primary
public class EmailService implements MessageService { }

2. @Qualifier — 明示的に選択:

java
public OrderService(@Qualifier("emailService") MessageService msg) { }

3. List で受け取る — すべての実装を受け取る:

java
public OrderService(List<MessageService> all) {
    // すべての MessageService 実装
}

よくある落とし穴

循環参照:A が B を注入され、B が A を注入される場合。コンストラクタ注入では 起動時にエラー。設計が間違っているサインです — 共通部分を新しいクラスに切り出す のが正解。

@Autowired vs @Resource vs @Inject:Spring 標準の @Autowired だけ知っていれば十分。他の2つは Java 標準 ですが、実務ではほとんど使われません。

まとめ

  • コンストラクタ注入が標準。Lombok の @RequiredArgsConstructor との組み合わせを推奨
  • 同じ型の Bean が複数ある場合は @Primary@Qualifier
  • 循環参照は 設計上のサイン — リファクタリングが必要

Bean — *Springが管理するオブジェクト*

Bean とは何か

Spring IoC コンテナに 登録・管理されるオブジェクトBean です。小さな単位のオブジェクトの集まりという比喩。通常の Java オブジェクトとの違いは、Spring がその生成・ライフサイクル・注入をすべて管理する という点です。

Bean の登録方法

1. @Component 系(最も一般的)

java
@Component        // 一般
@Service          // ビジネスロジック
@Repository       // DB アクセス
@Controller       // ウェブコントローラー
@RestController   // REST API

名前は違いますが、基本的に Bean を登録する という点は同じです。意味的な区別と一部の機能差があります(@Repository は例外変換を行う)。

java
@Service
public class UserService {
    // 自動的に Bean として登録される
}

2. @Bean(メソッド単位)

設定クラスのメソッドの戻り値を Bean として登録:

java
@Configuration
public class AppConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
}

自分のコードでは ない 外部ライブラリのクラス(RestTemplate・ObjectMapper 等)を Bean として登録する際に使用。

3. 自動設定 — Spring Boot Starter

spring-boot-starter-data-jpa のような starter 依存関係が 自動的に 多数の Bean を登録します(DataSource・EntityManager・TransactionManager 等)。

Bean スコープ — いつ新しく作るか

デフォルトは singleton — コンテナに 1つだけ 作って全員で共有。ただし他の選択肢もあります。

スコープ意味用途
singleton(デフォルト)コンテナあたり1個ほぼすべての場合
prototypeリクエストごとに新しいオブジェクト状態を持つオブジェクト
requestHTTP リクエストあたり1個リクエスト単位の状態
sessionHTTP セッションあたり1個セッション単位の状態
applicationServletContext あたり1個グローバル
java
@Service
@Scope("prototype")
public class StatefulProcessor { }

最もよくある落とし穴:singleton Bean に 変更されるフィールド を持つこと。マルチスレッド環境では データが壊れる。常に 不変 または 外部ストア(DB・Redis)を使いましょう。

Bean のライフサイクル

Bean は以下の段階を経ます:

1. インスタンス生成(コンストラクタ呼び出し)
2. 依存性注入@Autowired 等)
3. 初期化@PostConstruct または InitializingBean.afterPropertiesSet()
4. 使用
5. 破棄@PreDestroy または DisposableBean.destroy()

java
@Service
public class CacheManager {
    @PostConstruct
    public void init() {
        // Bean 生成後 1 回実行。キャッシュのロードなど
    }

    @PreDestroy
    public void cleanup() {
        // アプリ終了時に 1 回実行。リソースのクリーンアップ
    }
}

@PostConstruct は頻繁に使われます — リソースの準備・キャッシュのウォームアップ等。@PreDestroyGraceful Shutdown に重要です。

@Configuration vs @Component

どちらも Bean として登録されますが、役割が異なります

  • @Configuration — 設定クラス。内部の @Bean メソッドを プロキシでラップして 同一インスタンスの返却を保証
  • @Component — 通常の Bean

@Configuration 内で @Bean メソッドが 互いを呼び出す同一インスタンス が返ります。@Component だと毎回新しいオブジェクトが作られるバグが発生する可能性があります。

まとめ

  • Bean = Spring が管理するオブジェクト
  • 登録:@Service 等のアノテーション、または @Bean メソッド
  • デフォルトスコープは singleton(ほとんどの場合に適切)
  • @PostConstruct@PreDestroy でライフサイクルフックが可能

@Bean · @Configuration · @Qualifier · @Primary の競合解決

@Component vs @Bean — どちらをいつ使うか

  • @Component(@Service・@Repository・@Controller 含む)— 自分で作成したクラス に。Spring が自動スキャンします。
  • @Bean外部ライブラリのオブジェクト または 条件付き登録 が必要なとき。

@Configuration + @Bean の例

java
@Configuration
public class AppConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplateBuilder()
            .setConnectTimeout(Duration.ofSeconds(3))
            .setReadTimeout(Duration.ofSeconds(5))
            .build();
    }

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper()
            .registerModule(new JavaTimeModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }
}

外部ライブラリのクラス(RestTemplate・ObjectMapper) は自分で @Component を付けられないため、@Bean で登録します。

同じ型の Bean が2つ — 競合

java
@Bean PaymentService cardPayment() { ... }
@Bean PaymentService kakaoPayment() { ... }

@Autowired
PaymentService paymentService;   // ❌ NoUniqueBeanDefinitionException

同じ型が複数 → Spring が どれを注入すればよいかわからない

解決策1 — @Qualifier で名前を指定

java
@Autowired
@Qualifier("kakaoPayment")
PaymentService paymentService;

Bean の名前は メソッド名 がデフォルト(cardPaymentkakaoPayment)。

解決策2 — @Primary でデフォルトを選択

java
@Bean @Primary
PaymentService cardPayment() { ... }   // デフォルト値

@Bean
PaymentService kakaoPayment() { ... }

@Autowired PaymentService p;   // ✅ cardPayment 注入 (Primary)

どちらを使うか

  • デフォルト・主力実装が明確@Primary
  • 状況によって異なるものを注入@Qualifier
  • どちらも使わず名前で自動マッチング も可能(@Autowired PaymentService cardPayment;

条件付き登録 — @ConditionalOnProperty

java
@Bean
@ConditionalOnProperty(name = "payment.provider", havingValue = "kakao")
public PaymentService kakaoPayment() { ... }

application.yml の payment.provider=kakao のときだけ Bean を登録します。プロファイルごとに異なる実装 をきれいに分離できます。

🤖 AI にこう依頼してみましょう

このレッスンの概念を知っていれば、AI に 具体的に 指示できます。漠然とした「直して」ではなく、語彙を持ったリクエスト — それがトークン節約の出発点です。

  • 「この PaymentService の実装が2つあるので @Qualifier で分岐させてください」
  • 「この RestTemplate を @Configuration + @Bean で登録してください」
  • 「@ConditionalOnProperty でカカオ決済モジュールを条件付きで有効化してください」

なぜトークンが減るのか

概念を知らないと、AIの回答を受け取っても 「それは何ですか?」 と再度質問しなければなりません。その「再質問」がトークンを消費します。概念を一度理解しておけば、会話が一度で終わります

IoC · DI · Bean — Spring の心臓 - Spring Boot