この記事は弥生 Advent Calendar 2022の9日目の記事です。
2022年10月に入社した情報システム部エンジニアの小坂です。
Spring Bootで開発をすると必ず出会うのがDI。
でもこのDI、自分の言葉で上手く説明できるでしょうか?
今回はそんなSpringのコア技術であるDIについて分かりやすく解説をしていきます。
※今回記載したコードは下記のバージョンで動作確認をしています。
Spring Boot 2.7.6
Spring BootのDI
DIって何?
DIは「dependency injection」の略で、日本語では「依存性の注入」と訳されます。
依存性?注入?と何だか分かりにくい表現ですが、
Springによるオブジェクトのインスタンス管理の仕組みになります。
どんな仕組みなのでしょうか?順番に見ていきましょう。
※情報元によってはDIではなくIoCと記載して説明している場合もあります。Springの公式サイトでは下記のように記載されています。
IoC は、依存性注入(DI)とも呼ばれます。
「依存性」とは?
例えば次のようなControllerクラスがあります。
CatController.java
@RestController public class CatController { @GetMapping("/cat") public Cat cat(@RequestParam(value="name", defaultValue="yayoi") String name){ CatService catService = new CatServiceImpl(); Cat cat = catService.eat(name); return cat; } }
CatControllerのcat
メソッドではCatServiceImplクラスのオブジェクトを生成して利用しています。
つまり「CatControllerのcat
メソッドはCatServiceImplのインスタンスが必要であり、依存している」ともいえるのです。
これが依存性になります。
なお、上記の例はCatControllerがCatServiceImplを直接生成しており、これはDIではありません。
次に、注入の説明の前にSpringのオブジェクト管理の仕組みを見てみましょう。
DIコンテナとBean
SpringはDIコンテナと呼ばれる基盤でオブジェクトのインスタンスを管理しています。
このDIコンテナで管理されるコンポーネントをBeanと呼びます。
そして、DIコンテナからBeanを元にインスタンスが生成され、各コンポーネントで利用されます。
なお、SpringではデフォルトでSingletonのため基本的にはDIコンテナで生成された同一インスタンスが再利用されますが、インスタンスを必要に応じて都度生成したり、Webのリクエスト毎に生成するような記述も可能です。
※Singleton以外を設定する仕組みについてはここでは詳しくは触れません。以下、SingletonのBean前提で記載します。
Beanを定義する
Beanとして定義されるのはSpring Frameworkが提供しているクラスの他に、
アプリ開発者が定義したクラスも含まれます。
アプリ開発者がBean定義する場合は、該当のクラスに@Component
というアノテーションを記述します。
@Component
を付けたクラスは、後述するコンポーネントスキャン等の設定により、BeanとしてDIコンテナに登録されるようになります。
Bean定義するアノテーションは、他にも下記のようなものがあります。
アノテーション | 説明 |
---|---|
@Controller |
MVCモデルのコントローラクラス用 |
@Service |
ビジネスロジッククラス用 |
@Repository |
データアクセスクラス用 |
@Component |
上記以外 |
@Controller
、@Service
、@Repository
についてはステレオタイプアノテーションとも呼ばれ、@Component
を内包しています。
例えば@Controller
のソースを見てみると、@Component
が利用されていることが分かります。
Controller.java
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Controller { 以下省略
そして、Springが@Component
アノテーションのあるクラスを探し出し、DIコンテナへBeanとして登録、その後インスタンスを生成するという流れになります。
DIコンテナへBeanを登録
@Component
アノテーションのあるクラスを探す方法はいくつかありますが、Spring Bootではコンポーネントスキャンという方法が使われています。
これは我々アプリ開発者があまり意識することはありません。下記のコードを見てみましょう。
おなじみ、Spring Bootのアプリケーションを起動するクラスですね。
CatsampleApplication.java
@SpringBootApplication public class CatsampleApplication { public static void main(String[] args) { SpringApplication.run(CatsampleApplication.class, args); } }
上記の@SpringBootApplication
アノテーションの中身を見てみると、下記の通りです。
SprintBootApplication.java
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { 以下省略
上記で@ComponentScan
というアノテーションが使われています。
@ComponentScan
は、指定したパッケージのほか、自己クラスが属するパッケージ以下で@Component
があるクラスを探すという動作をします。
そのため、@Component
があるクラスはSpring Bootの起動時に自動的に走査され、DIコンテナへSingletonのBeanとして登録されます。
前置きが長くなりましたが、注入をここから見ていきます。
依存性の注入
最初の例でお見せしたCatControllerを再掲します。
CatController.java
@RestController public class CatController { @GetMapping("/cat") public Cat cat(@RequestParam(value="name", defaultValue="yayoi") String name){ CatService catService = new CatServiceImpl(); Cat cat = catService.eat(name); return cat; } }
上記の例では、下記の課題があります。
- CatServiceImplのインスタンスはcatメソッドが呼ばれる度に生成されます。
例えばWebサービスで1万回リクエストがあれば、1万回インスタンスが生成されることを意味し、メモリの利用効率が悪いです。 - 上記のコードを単体テストしたい場合、CatServiceImplクラスがまだ出来ていなければ単体テストができませんし、出来ていたとしてもCatServiceImplクラスの処理を考慮したテストケースとなるため、テストが複雑になります。つまり、クラス間の結合度が高い点が課題です。
これを解決するのが注入です。 CatServiceImplをDIするように変更すると、下記のようなコードになります。
CatController.java
@RestController public class CatController { private final CatService catService; @Autowired public CatController(CatService catService) { this.catService = catService; } @GetMapping("/cat") public Cat cat(@RequestParam(value="name", defaultValue="yayoi") String name){ Cat cat = catService.eat(name); return cat; } }
CatServiceImpl.java
@Service public class CatServiceImpl implements CatService{ public Cat eat(String name) { Cat cat = new Cat(name); // 何か処理 return cat; } }
@Service
というアノテーションを指定することで、CatServiceImplクラスがBeanとして登録されます。
そして、CatControllerクラスでは@Autowired
というアノテーションを付けてCatServiceを定義しています。
private final CatService catService; @Autowired public CatController(CatService catService) { this.catService = catService; }
@Autowired
を付加することで、DIコンテナが先ほどのCatServiceImplクラスのBeanのインスタンスを生成し、CatControllerのcatServiceに代入します。
これが依存性の注入の正体です。
つまり、CatControllerクラスが依存するオブジェクトのインスタンスを直接生成するわけではなく、「SpringのDIコンテナにインスタンスを生成してもらって代入してもらう」から注入というわけです。
@Autowired
で定義したCatServiceはインタフェースですが、実際に代入されたのはCatServiceImplクラスのインスタンスです。
DIを行うと、対象インタフェースの実装クラスをBeanから探して代入してくれるという動きをします。
これは後ほど説明しますが、実装クラスを容易に切り替えることができるというメリットにつながります。
なお、@Autowired
を利用してDI対象のオブジェクトを定義する方法は
- セッターインジェクション
- フィールドインジェクション
- コンストラクタインジェクション
の3通りの書き方があり、上記で記載したコードは3番目のコンストラクタインジェクションになります。
コンストラクタインジェクションのみfinal宣言をすることが可能で、誰かが誤ってフィールドをnewしようとした時にコンパイルエラーになるため、より安全です。
DIの注意点
Beanは基本的にはSingletonオブジェクトとなるため、インスタンス変数を保持するようなクラスをBeanに登録しDIするのは避けたほうがよいです。
例えば、下記のようなクラスです。
SampleComponent.java
@Component public class SampleComponent{ int count = 0; public void hoge(){ count++; // 処理 } }
上記の例では、Beanが利用される度にインスタンス変数が変更されてしまいます。
Beanにするクラスはインスタンス変数を保持しないように気を付けましょう。
同じ理由で、DTOやEntityなど値を保持するクラスはDIしないほうがよいでしょう。
DIのメリット
DIを使わない場合の課題をもう一度見てみましょう。DIを使うとどんなメリットがあるのでしょうか。
- CatServiceImplのインスタンスはcatメソッドが呼ばれる度に生成されます。
例えばWebサービスで1万回リクエストがあれば、1万回インスタンスが生成されることを意味し、メモリの利用効率が悪いです。
→DIはデフォルトでSingletonのため、一つのインスタンスを再利用します。結果的にメモリ利用の無駄が無くなります。
- 上記のコードを単体テストしたい場合、CatServiceImplクラスがまだ出来ていなければ単体テストができませんし、出来ていたとしてもCatServiceImplクラスの処理を考慮したテストケースとなるため、テストが複雑になります。つまり、クラス間の結合度が高い点が課題です。
→単体テスト時には別のMockクラスをDIすることでCatServiceImplクラスの処理を気にせずにテストすることができます。つまり、クラス間を疎結合な構成にすることが可能です。
また、今回詳しくは触れませんが、DIコンテナで生成されたインスタンスのライフサイクル管理をSpringが自動的に行ってくれる点もメリットの一つです。
あとがき
Spring Bootでの開発では、Springのコア技術であるDIがどのような仕組みなのか理解しておくことはとても大切です。
DIについて掘り下げてみると今回の記事で説明していない点、深い点はまだまだたくさんあります。
スムーズに開発をするためにも、Springの技術について理解を深めておきたいですね。
一緒に働く仲間を募集しています
弥生では一緒に働く仲間を募集しています!
弥生で働くことに興味がありましたら、求人一覧をぜひご覧ください。