今さら聞けないSpring BootのDI

この記事は弥生 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の技術について理解を深めておきたいですね。

一緒に働く仲間を募集しています

弥生では一緒に働く仲間を募集しています!
弥生で働くことに興味がありましたら、求人一覧をぜひご覧ください。

herp.careers