sasaryo.dev  |  blog   about

Spring IoCコンテナとは何か

#spring

はじめに

Springを触りはじめると、まず@Autowired@Componentといったアノテーションに出会います。

見よう見まねで付けてみると、なぜか動く。動くのだけれど、「これは裏で何をしてくれているんだろう」というモヤモヤが残りました。

このモヤモヤは、おそらく「IoCコンテナ」という仕組みを飛ばして、アノテーションの使い方だけを覚えてしまったことから来ています。逆に、IoCコンテナが何を引き受けているのかを一度押さえると、Springの入口に並ぶアノテーションが「ああ、この仕事を頼んでいたのか」と一本につながってきます。

この記事では、newを使って依存関係を自分で組み立てるところから始めます。そこからDI、IoC、Bean、コンテナ、登録、注入という順番で見ていきます。

自分でnewする:密結合

たとえば、注文を処理するOrderServiceが、メールを送信するMailSenderを必要としているとします。

素直に書くと、次のようになります。

public class OrderService {
    private final MailSender mailSender = new SmtpMailSender();

    public void place(Order order) {
        // 注文処理...
        mailSender.send(order.getUserEmail(), "ご注文ありがとうございます");
    }
}

このコードでも、注文処理は動きます。ただ、OrderServiceの中でSmtpMailSender直接newしていることが、いくつか面倒な問題を引き起こしてしまいます。

たとえばテストでは、実際にメールが送信されると困るので、モックへ差し替えたくなります。本番環境ではSMTPを使い、開発環境ではログに出すだけの実装へ切り替えたいこともあります。

ところが、どちらの場合もOrderServiceの中にあるnew SmtpMailSender()が邪魔になります。使う実装を変えるたびに、OrderService自身を書き換えなければならないからです。

つらさの原因は、newという構文そのものではありません。OrderServiceという「使う側」が、SmtpMailSenderという具体的な実装だけでなく、その作り方まで知っていることにあります。

使う側と使われる側が強く結びついている状態。ここでは、これを密結合と呼びます。

この先で考えたいのは、単にnewという文字を消す方法ではありません。OrderServiceSmtpMailSenderの結びつきを、どうやってほどくかという話です。

依存を外から受け取る:DI

つらさの原因は、OrderServiceが必要なオブジェクトを自分で作っていることでした。

それなら、MailSenderを自分で作るのをやめて、外から受け取る形にしてみます。

public class OrderService {
    private final MailSender mailSender;

    // 自分ではnewせず、外から渡してもらう
    public OrderService(MailSender mailSender) {
        this.mailSender = mailSender;
    }

    public void place(Order order) {
        // 注文処理...
        mailSender.send(order.getUserEmail(), "ご注文ありがとうございます");
    }
}

OrderServiceの中からnew SmtpMailSender()が消えました。

いまのOrderServiceが知っているのは、「自分にはMailSenderが必要だ」ということだけです。実際にどの実装を使うのかは、外側で決められるようになりました。

必要なオブジェクトを利用する側で作るのではなく、外から渡してもらうこと。これが**DI(Dependency Injection / 依存性注入)**です。

ただ、「外から渡す」といっても、その外側では誰かがオブジェクトを作らなければなりません。自分で組み立てるなら、たとえば次のようになります。

public static void main(String[] args) {
    MailSender mailSender = new SmtpMailSender();
    OrderService orderService = new OrderService(mailSender);

    // ...
}

OrderServiceから消えたnewが、mainへ移動しています。

このように、アプリケーションで使うオブジェクトを作り、依存関係を組み立てる場所は、composition rootと呼ばれます。オブジェクト同士をつなぐ配線場所のようなものです。

この段階でも、DIは成立しています。Springを使わず、自分でコンストラクタへ依存を渡しているので、手動DIと呼ばれる方法です。

小さなアプリケーションであれば、手動DIでもそれほど困りません。むしろ、どのオブジェクトがどこで作られているか分かりやすく、ちょうどよいこともあります。

困りはじめるのは、アプリケーションが育ってきたときです。

オブジェクトが何十、何百と増え、依存関係も何段にも重なっていき、new A(new B(new C(...)))のような組み立てをcomposition rootへ書き続け、依存を一つ追加するたびに配線を直す。生成や初期化、破棄のタイミングまで自分で面倒を見る必要も出てきます。

ここまで来ると、組み立てをすべて自分で管理するのは正直つらい。

そこで、この「オブジェクトを作って組み立てる仕事」そのものを、別の仕組みに任せたくなります。

IoC:組み立ての主導権をコンテナへ渡す

Springでは、オブジェクトの生成や組み立てをコンテナに任せられます。

先ほどmainへ書いた組み立て処理を、コンテナという管理役に預けるイメージです。

こちらが用意するのは、「OrderServiceにはMailSenderが必要」といった情報です。実際にどのオブジェクトを生成し、どの依存を渡し、いつ初期化するかはコンテナが判断します。

オブジェクトを使う側ではなく、外側の仕組みが生成や呼び出しの流れを制御すること。これが**IoC(Inversion of Control / 制御の反転)**です。

よく「こちらから呼ぶのではなく、向こうから呼ばれる」と表現されます。自分のコードがすべての処理を指揮するのではなく、フレームワークが全体の流れを持ち、その中で自分のコードが呼び出される形です。

DIは、このIoCを実現するために使われる代表的な方法です。

先ほどコンストラクタからMailSenderを受け取るようにした時点で、OrderServiceは生成方法の制御を外側へ渡しています。Springでは、その外側の役割をIoCコンテナがまとめて引き受けてくれる、という関係です。

たとえるなら、部品を自分で買い集めて組み立てるのをやめて、必要な構成を工場へ伝えるようなものだと思います。工場が部品を用意して組み立て、完成したものを渡してくれる。

この工場にあたるのが、SpringのIoCコンテナです。

Beanとコンテナ:何を管理してくれるのか

ここからは、Springの用語をもう少し具体的に見ていきます。

Bean : SpringのIoCコンテナによって生成・管理されるオブジェクト。

先ほど登場したOrderServiceSmtpMailSenderも、コンテナへ登録すればBeanになります。コンテナはBeanの定義をもとにオブジェクトを生成し、必要な依存関係を組み立てます。

SpringでIoCコンテナへアクセスするとき、中心になるのがApplicationContextです。

より基礎的な機能を持つBeanFactoryがあり、そこへイベント通知や国際化などの機能を加えたものがApplicationContext、という関係になっています。Springの公式リファレンスでも、通常はApplicationContextの利用が推奨されています

細かな違いはありますが、最初は「SpringのBeanを管理する中心がApplicationContext」という理解で十分だと思います。

コンテナへBeanを登録する

コンテナに組み立ててもらうには、まず「どのクラスをBeanとして扱うのか」を教える必要があります。

登録方法には、アノテーションを使う方法、Javaの設定クラスへ記述する方法、XMLへ記述する方法などがあります。ここでは、よく目にするアノテーションを使った方法を見ていきます。

まず、Beanとして登録したいクラスへ@Componentを付けます。

@Component
public class SmtpMailSender implements MailSender {
    // ...
}

次に、@ComponentScanを使って、@Componentが付いたクラスを探す範囲を指定します。

@Configuration
@ComponentScan(basePackages = "com.example.shop")
public class AppConfig {
}

これで、com.example.shop以下にある@Component付きのクラスが検索され、Beanとしてコンテナへ登録されます

Spring Bootを使っている場合は、@SpringBootApplicationの中に@ComponentScanの働きが含まれています。そのため、自分で@ComponentScanを書いていなくても、起動クラス以下のパッケージが自動で検索されることが多いです。

@Componentには、役割ごとに名前が付いた仲間もあります。

@Service : 主にサービス層のクラスへ付けるアノテーション。

@Repository : 主にデータアクセス層のクラスへ付けるアノテーション。Springによる例外変換の対象にもなる。

@Controller : Spring MVCで、Webリクエストを受け取るクラスへ付けるアノテーション。

いずれも、Beanとして登録されるという基本的な働きは@Componentと共通しています。ただし、単なる見た目の違いではありません。

たとえば@Repositoryは、適切な設定のもとで、データアクセス時に発生した例外をSpring共通のDataAccessException体系へ変換するための目印になります。@Controllerは、Spring MVCがリクエストの送り先を探すときに使われます。

とはいえ最初は「Beanとして登録しつつ、そのクラスの役割も表すアノテーション」くらいに捉えておくと、理解しやすい気がします。

登録したBeanを注入する

Beanの登録が済んだら、次は必要な場所へ渡してもらいます。

OrderServiceもBeanとして登録し、コンストラクタからMailSenderを受け取るようにします。

@Component
public class OrderService {
    private final MailSender mailSender;

    public OrderService(MailSender mailSender) {
        this.mailSender = mailSender;
    }
}

Springはコンストラクタの引数を見て、MailSender型に合うBeanをコンテナから探します。今回であれば、MailSenderを実装したSmtpMailSenderが渡されます。

コンストラクタが一つだけの場合、現在のSpringでは@Autowiredを省略できます。付けて書くなら、次の形です。

@Component
public class OrderService {
    private final MailSender mailSender;

    @Autowired
    public OrderService(MailSender mailSender) {
        this.mailSender = mailSender;
    }
}

これで、最初のコードにあったnew SmtpMailSender()OrderServiceから消え、オブジェクトの生成と依存関係の組み立ては、コンテナが引き受けています。

ただし、MailSenderを実装したBeanが複数ある場合は、型だけでは注入先を一つに決められません。

@Component
public class OrderService {
    private final MailSender mailSender;

    public OrderService(
            @Qualifier("smtpMailSender") MailSender mailSender
    ) {
        this.mailSender = mailSender;
    }
}

このような場合は、@Qualifierを使うと注入するBeanを指定できます。ほかにも、一つを優先候補にする@Primaryという方法があります。

なぜ手動DIで止めず、コンテナへ任せるのか

ここまでで、コンテナへ組み立てを任せる流れが見えてきました。

手動DIでも、具体的な実装を外から渡せるため、クラス同士の結びつきを弱めやすくなります。それでもSpringのコンテナを使うのは、依存を注入する以外の仕事もまとめて任せられるからです。

オブジェクトの組み立てを自動化できる

手動DIでは、依存関係が増えるたびにcomposition rootの配線を直します。

コンテナを使うと、Beanの定義や型をもとに、依存関係のグラフを組み立ててもらえます。オブジェクトが増えても、すべてのnewとコンストラクタ呼び出しを一か所へ並べ続ける必要はありません。

アプリケーションが大きくなるほど、この差は効いてきます。

ライフサイクルやスコープを管理してもらえる

Beanをいつ生成し、いつ初期化し、いつ破棄するのか。インスタンスを一つだけ使うのか、必要になるたびに作るのか。

こうした管理も、コンテナの役割です。

自分で実装することもできますが、毎回同じ仕組みを作るのは大変です。Springの仕組みに乗ることで、アプリケーション固有の処理へ集中しやすくなります。

Beanの処理を外側から包める

個人的に、コンテナを使う大きな理由だと感じたのがこの部分です。

SpringはBeanを生成する過程へ処理を差し込み、必要に応じて元のBeanをプロキシで包めます。これにより、メソッドの前後へ共通処理を追加できるようになります。

たとえば@Transactionalを付けると、トランザクションの開始やコミット、ロールバックを宣言的に扱えます。キャッシュや認可、ログなどの処理にも、同じ考え方が使われています。

手動DIでも、自分でプロキシやデコレーターを組み立てれば、似た仕組みは実現できます。ただ、その組み立てまで自分で管理することになります。

コンテナがBeanの生成を握っているからこそ、こうした共通処理をまとめて適用しやすい。Springの便利な機能が、IoCコンテナの上に積み重なっている感覚が、少しずつ見えてきました。

まとめ

ここまでの流れを整理します。

最初は「付けたら動いた」という印象だった@Component@Autowiredも、いまではそれぞれ「コンテナへの登録」と「依存の注入」に関わる道具として見えるようになりました。

IoCコンテナは、単にnewを代わりに書いてくれる装置ではありません。Springのさまざまな機能が動くための土台でもあります。

次は、コンテナがBeanをプロキシで包むと何が起きるのか、@TransactionalやAOPの仕組みまで追っていきたいと思います。