日々をより良く

〜てくのろじーをそえて〜

Spring Boot + Kotlin で依存性を注入する

f:id:nbtk39:20211217092812p:plain

この記事は以下の続編です!

nbtk.hatenablog.com

前回は以下の「体系的に依存性の注入の理解を深める」ための整理をしてきました。

  • 体系的に依存性の注入の理解を深める
    • そもそもの課題
    • DI、DI Container
    • DI を知る上で必要な概念
      • 依存性逆転の原則
      • 制御の逆転

今回は以下のフレームワークを利用した DI の実装」「他に DI で知ってるといいこと」についてお話しします!

  • フレームワークを利用した DI の実装
    • Kotlin + Spring Boot での実装方法
    • DI のスコープについて
    • 注入の種類(コンストラクタ・セッター・フィールド)
  • 他に DI で知ってるといいこと
    • Service Locator
    • 疎結合にしすぎるデメリット

🙂 Spring Boot の例が必要ない方でも、「対象クラスを DI Container に登録する」の項目を読み飛ばして進めていただくと、ざっくりしたイメージは掴んでいただけると思います。


各バージョン

macOS Monterey 12.0.1

Kotlin 1.6.0

Spring Boot 2.5.7


DI Container を利用する

Spring では公式ドキュメントによると DI Container ではなく IoC Container と呼んでいるようです。この記事ではわかりやすいように DI Container で統一してお話しします。

前回の記事では Application クラスが DI をしていました。IoC により DI をフレームワークが吸収してくれるため、Applicaiton クラスを BookController というコントローラクラスに置き換えて説明します。

f:id:nbtk39:20211217093421p:plain

対象クラスを DI Container に登録する

Spring Boot ではアノテーションを付与するだけで簡単に DI の管理ができます。

@SpringBootApplication
class ExampleDependencyInjectionApplication

fun main(args: Array<String>) {
    runApplication<ExampleDependencyInjectionApplication>(*args)
}

@RestController
@RequestMapping("/books")
class BookController(
    private val bookListService: BookListService
) {
    @GetMapping
    fun getBooks(): List<String> {
        return bookListService.getAllBooks()
    }
}

interface BookListService {
    fun getAllBooks() : List<String>
}

@Service
class BookListServiceImpl(
    private val bookRepository: BookRepository
): BookListService {
    override fun getAllBooks() : List<String> {
        return bookRepository.getAll()
    }
}

interface BookRepository {
    fun getAll(): List<String>
}

@Repository
class BookRepositoryImpl: BookRepository {
    override fun getAll(): List<String> {
        return File("booklist.txt").readLines()
    }
}

なんとこれだけで DI してくれます!@RestController @Service @Repository アノテーションを付与しただけです。 簡単ですね。むしろ簡単すぎてわかりづらいですね!仕組みを解説します。

Spring Boot では @Component をクラスに付与することで DI 対象になります。

サンプルコードでは @RestController @Service @Repository を付与しましたが、それぞれのアノテーション@Component を継承しています。なので DI Container に登録できたわけです。

また、詳細は後述しますが Spring Boot はデフォルトで Singleton インスタンスが生成されます。つまり、インジェクト対象の変数全てに同一インスタンスがセットされます。

@Component @RestController @Service @Repository 何が違う??

なお @Component とそれ以外の上記のアノテーションが何が違うかというと、特に違いはありません!人間が見て責務がわかりやすいようにつけています。

例えば @Service のクラスとドキュメントは以下です。(一部抜粋)

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service { ... }

Indicates that an annotated class is a "Service", originally defined by Domain-Driven Design (Evans, 2003) as "an operation offered as an interface that stands alone in the model, with no encapsulated state."

ドメイン駆動設計によって定義されているサービスクラスを示す、と書いています。

@RestController @Repository なども同様で、ドメイン駆動設計に基づいて実装されているようです。

注入(インジェクト)する仕組み

Spring Boot では @SpringBootApplication アノテーションにより@SpringBootConfiguration, @EnableAutoConfiguration, @ComponentScan が有効になります。これらの仕組みにより、自動的に DI の対象クラス(@Component が付与されたクラス)を DI Container に登録し、さらに必要な箇所にインスタンスを自動的にインジェクトしてくれます。これをコンポーネントスキャンと言います。便利ですね!

また、覚えておきたい定義に ConfigurationBean があります。Spring では DI 対象にするクラスを Bean といいます。ConfigurationBean を定義するクラスファイルです。@SpringBootApplication だけを利用している場合は無意識に使うこともできますが、内部では Configuration と Bean を定義しています。

@SpringBootApplication 以外にも DI する仕組みがあり、その一つに Java Config があります。Java Config は以下のように書きます。

@Configuration
public class AppConfig { 
  
    @Bean
    public BookRepository bookRepository() {
        return new BookRepositoryImpl();
    }
}

@Configuration@Bean を定義することにより DI Container に登録するインスタンスを定義しています。(コンポーネントスキャンと Java Config は併用可能)

@SpringBootApplication の各アノテーションのポイントは以下です。

  • @SpringBootConfiguration
    • Configuration クラスであることを定義しています。 @Configuration を継承しています。
  • @ComponentScan
    • このアノテーションが付与されたパッケージは以下の Bean をスキャンします。
  • @EnableAutoConfiguration
    • Auto-configuration を有効にします。これにより、Bean の登録や DI を行ってくれます。

省略可能だけど大事な @Autowired

先ほどのサンプルコードの中に省略されているアノテーションが存在します。 @Autowired です。

@Autowired を付与することで Bean をインジェクトしてくれます。

ただし、Spring 4.3 からは単一コンストラクタの場合、 @Autowired を省略可能になりました。

省略せずに実装すると以下になります。(一部抜粋)

@RestController
@RequestMapping("/books")
class BookController(
    @Autowired private val bookListService: BookListService // ⇦ココ
) {
    @GetMapping
    fun getBooks(): List<String> {
        return bookListService.getAllBooks()
    }
}

@Service
class BookListServiceImpl(
    @Autowired private val bookRepository: BookRepository  // ⇦ココ
): BookListService {
    override fun getAllBooks() : List<String> {
        return bookRepository.getAll()
    }
}

DI 対象のインスタンスを保持するスコープ

先ほど「Spring Boot はデフォルトで Singleton インスタンスが生成されます」とお話ししました。

インスタンスの生成・破棄のタイミング(スコープ)には種類があり、 singleton prototype request session application websocket があります。

詳細は公式ドキュメントを参照していただきたいのですが、注意すべき点は一番多く利用すると思われるデフォルトの singleton の場合、Bean がフィールドを保持しているとプログラムに不具合が発生する可能性が高いということです。

注入(インジェクション)の種類

さて、ここまででだいーぶ整理できてきました!ここまで付き合ってくださったみなさまみなさまありがとうございます。ただあともう少し説明させてください。

DI には種類があり、 コンストラクタインジェクション セッターインジェクション フィールドインジェクション があります。それぞれ解説します。

コンストラクタインジェクション

サンプルコードでは コンストラクタインジェクション を利用しました。これが一番安全で推奨されています。

サンプルコードをもう一度見てみましょう。コンストラクタに変数を定義してインジェクトされていることがわかります。

@RestController
@RequestMapping("/books")
class BookController(
    private val bookListService: BookListService // ⇦ココ
) {
    @GetMapping
    fun getBooks(): List<String> {
        return bookListService.getAllBooks()
    }
}

コンストラクタインジェクションだと後から変数を変更できません。これが安全な理由です。

セッターインジェクション

実装は以下のようになります。

@RestController
@RequestMapping("/books")
class BookController {

    // ↓ココ
    private var bookListService: BookListService? = null
        @Autowired
        set(bookListService) {
            this.bookListService = bookListService
        }

    @GetMapping
    fun getBooks(): List<String> {
        return bookListService.getAllBooks()
    }
}

セッターに @Autowired しています。後から代入するため var で宣言して null を代入しています。

変更可能性があるのでちょっと危険ですね。

フィールドインジェクション

実装は以下のようになります。

@RestController
@RequestMapping("/books")
class BookController {

    // ↓ココ
    @Autowired
    lateinit var bookListService: BookListService

    @GetMapping
    fun getBooks(): List<String> {
        return bookListService.getAllBooks()
    }
}

フィールドに変数を宣言しています。

Kotlin のフィールド変数は宣言時に初期化が必要なので、DI の場合は lateinit を修飾して、後から代入するため var にする必要があります。

変更可能性があるのでちょっと危険ですね。

他に DI で知ってるといいこと

Appendix 的な感じですが、補足で知っておくと良いことをお話しします。

Service Locator

これまで依存性逆転の法則を実現する方法として DI Container を解説してきました。しかし他にも手段があります。Service Locator です。

Service Locator はDI対象のインスタンスを生成・保持しているクラスです。

DI Container はフレームワークによってインスタンス生成・保持などの振る舞いが隠蔽されることがありますが、Service Locator はそれ自体のインスタンスをシングルトンで生成し、依存性を注入する場所に Service Locator パターンが実装されたクラスのインスタンスを引数で受け取ります。

Service Locator のインスタンスを受け取ったクラスは、Service Locator から必要なDI対象のインスタンスを取り出します。

Service Locator は DI Container と比較して主に以下の理由で推奨されておりません。

  • Service Locator 自体に依存してしまう
  • Service Locator で保持しているインスタンスがどこに依存しているか分かり難い

より詳細な説明や実装例は以下が参考になりますのでぜひご覧ください。

Inversion of Control コンテナと Dependency Injection パターン

なんでも疎結合にすればいいというわけではない

ここまで、モジュール同士は疎結合に保とう!と行ってきました。

しかし、全てのモジュール同士に抽象を挟んで疎結合にすると、それだけコードが増えて人間が把握する手間や、実装・保守のコストがかさみます。

クラスだけでなく、DI Container の設定項目(ファイルやアノテーション)も増えてコードの可読性が低下してしまいます。

チームの設計方針にもよりますが、本当に必要な箇所だけ疎結合にされている方がメリットが大きいことが多いと思いますので、どこを疎結合にするかは慎重に決めたいですね。

おわりに

DI や DI Container、その周辺の概念について具体的な実装を交えて包括的にお話ししてきました。かなり長くなりましたが、付き合ってくださった方ありがとうございます。

DI まわりは概念的な話も多く、またフレームワークが吸収してくれるためわかりにくい部分もあります。百聞は一見にしかずと言うことで手を動かして馴染ませたいものですね!

参考にさせていただいた記事

コンテナから紐解く本当のSpring入門 #jsug / Understanding Spring Container - Speaker Deck

体系的に依存性の注入を理解する

f:id:nbtk39:20211216175322p:plain

この記事は、マネーフォワード関西拠点 Advent Calendar 2021 の17日目の投稿です🎄

16日目は iwami yuma さんの記事でした!

note.com

はじめに

主にオブジェクト指向かつ静的型付け言語のフレームワークでアプリケーションを開発していると、「依存性の注入」という概念をよく見かけます。

フレームワークがよしなに吸収してくれてなんとなく使えてるけど、正しく使えているのか、そもそもなんでこういう考え方があるのか気になることはありませんか?そしていざ調べてみると、なんとなくわかったようでわかってない、みたいなこともあると思います。実際にわたしがそうでした!

こんなことがわかります

ここではできるだけ体系的に、依存性の注入(DI / Dependency Injection) や DI Container を紐解いています。

  • 体系的に依存性の注入の理解を深める
    • そもそもの課題
    • DI、DI Container
    • DI を知る上で必要な概念
      • 依存性逆転の原則
      • 制御の逆転
  • フレームワークを利用した DI の実装
    • Kotlin + Spring Boot での実装方法
    • DI のスコープについて
    • 注入の種類(コンストラクタ・セッター・フィールド)
  • 他に DI で知ってるといいこと
    • Service Locator
    • 疎結合にしすぎるデメリット

この記事は2部構成です

書いてるとボリューミーになり、概念の説明と実装部分で大項目が2つに別れているので、この記事では 「体系的に依存性の注入の理解を深める」に焦点を当てて記載しています。

後半の「フレームワークを利用した DI の実装」「他に DI で知ってるといいこと」についてはこちらの記事をご覧ください!

nbtk.hatenablog.com

そもそもなんで依存性を注入するのか

依存性の注入の話に入る前に、そもそも解決したい課題のお話をします。

解決したいことは一言でいえば「モジュール同士を密結合にせずに、疎結合を保って変更に対して柔軟にする」ことです。

オブジェクト指向の設計において、SOLID という考え方がありますよね。ここでは全ては触れませんが、SOLID の中に「依存性逆転の原則 (Dependency Inversion principle)」というのがあります。

依存性の注入は、依存性逆転の原則から繋がってくるソフトウェアパターンになります。なので、まずは依存性逆転の原則についてお話しします。

依存性逆転の原則 (Dependency Inversion Principle) ってなに

まずは Wikipedia を読んでみます。

  1. 上位モジュールはいかなるものも下位モジュールから持ち込んではならない。双方とも抽象(例としてインターフェース)に依存するべきである。
  2. 抽象は詳細に依存してはならない。詳細(具象的な実装内容)が抽象に依存するべきである。

https://ja.wikipedia.org/wiki/依存性逆転の原則

・・・。言葉だとよくわからないので、図にします。

ここでは「保管している書籍の一覧を取得する」という超簡単なプログラムを例にします。

便宜的に Application(実行クラス) が Service を呼び出し、Service が Repository を呼び出す、というフローにします。

依存性逆転の原則に従っていない場合は例えば以下のようになります。

f:id:nbtk39:20211216144443p:plain

この時、それぞれのモジュールが依存している密結合な状態になります。いわゆる、上位モジュールが下位モジュールに依存している状態です。密結合の何が困るかというと、

  • BookRepository が修正されると BookListService も修正する必要がある(かもしれない)
  • BookListService の単体テストをするのに BookRepository が参照するデータも用意する必要がある。そうすると BookRepository が修正されるたびに BookListService にも影響してしまう

などです。

これを解決するには疎結合な状態にする必要があります。この手法が「依存性逆転の原則」です。引用した以下の部分です。

  1. 上位モジュールはいかなるものも下位モジュールから持ち込んではならない。双方とも抽象(例としてインターフェース)に依存するべきである。

これを実現すると、以下のモジュール設計になります。

f:id:nbtk39:20211216144447p:plain

(BookListService, BookRepository は抽象クラス、Impl は抽象の実装クラスと読み替えてください)

この時に以下の点に注意する必要があります。

2.抽象は詳細に依存してはならない。詳細(具象的な実装内容)が抽象に依存するべきである。

つまり、BookRepository が BookRepositoryImpl ありきで作られていると(依存していると)、間接的に BookListService は BookRepositoryImpl に依存していることになってしまいます。

なので、先に抽象(インターフェース)を要件に応じてメソッドやその引数を定義しておき、その抽象を実装する方向(実装が抽象に依存している状態)にすることが重要です。

上記は単純な例ですが、抽象を実装するクラスは複数になるケースも多いです。実装クラスベースの切り抜きで抽象が作られていると、うまく上位モジュールとして定義されていない状況が生まれてしまいますので注意が必要です。

これによって、各モジュールが疎結合な状態を維持できます。

で、何が嬉しいかというと、先ほどお話しした内容を解決できます。

BookRepository が修正されると BookListService も修正する必要がある(かもしれない)

BookRepositoryImpl が修正されたとしても、インターフェースが変わらない限りは BookListService に影響しません。

また、

BookListService の単体テストをするのに BookRepository が参照するデータも用意する必要がある。そうすると BookRepository が修正されるたびに BookListService にも影響してしまう

BookListService は BookRepository インターフェースに依存しています。なので、インターフェースを満たすモックを実装してあげて、単体テストではそのモックを参照するようにしてあげると、他のモジュールを気にせずにテスタビリティが確保できます。

これが依存性逆転の原則のいいところです。

では、インターフェースを実装したクラスを実際に使用するにはどのようにすればいいのでしょうか。

実はこの手段に、依存性の注入があります。

依存性を注入する

ここまでで抽象によってモジュールを疎結合に保つメリットについてお話ししてきました。前述した通り、疎結合に保つ方法として、依存性の注入があります。

依存性の注入というと何やらイメージが難しいのですが、言い換えると「オブジェクトの代入」です。

英語版Wikipedia では dependency injection is a technique in which an object receives other objects that it depends on と説明されています)

一気に簡単に思えてきましたね!

上記の書籍一覧の例でコードを書いてみます。

(サンプルコードには kotlin 1.6.0 を利用しています)

密結合なコード

class Application {
        val allBooks = BookListService().getAllBooks()
    allBooks.forEach {
        println(it)
    }
}

class BookListService {
    fun getAllBooks() : List<String> {
        return BookRepository().getAll()
    }
}

class BookRepository {
    fun getAll(): List<String> {
        return File("booklist.txt").readLines()
    }
}

依存性逆転の法則を適用したコード

class Application {
    val bookRepository = BookRepositoryImpl()
    val bookListService = BookListServiceImpl(bookRepository)
    val allBooks = bookListService.getAllBooks()
    allBooks.forEach {
        println(it)
    }
}

interface BookListService {
    fun getAllBooks() : List<String>
}

class BookListServiceImpl(
    private val bookRepository: BookRepository
): BookListService {
    override fun getAllBooks() : List<String> {
        return bookRepository.getAll()
    }
}

interface BookRepository {
    fun getAll(): List<String>
}

class BookRepositoryImpl: BookRepository {
    override fun getAll(): List<String> {
        return File("booklist.txt").readLines()
    }
}

インターフェースに依存することで、疎結合になっています。

例えば BookListService の単体テストを実装するには、コンストラクタ受け取る bookRepository にモックデータを返却する BookRepositoryMock クラスを作ってインスタンスを代入することでてスタビリティを確保できます。

依存性の注入とは

先ほど依存性の注入は「オブジェクトの代入」だと言いました。 上記の例だと、 BookListServiceImpl は依存するオブジェクトである BookRepository を受け取っています。言い換えれば、オブジェクトを代入しています。

このパターンが依存性の注入 (Dependency Injection) です。

DI Containerってなに

これまでで依存性の注入について紹介してきました。しかし、実務で依存するオブジェクトを代入するのはめちゃくちゃ大変ですよね。

大量のファイルに対して代入しないといけないし、何が何に依存しているのか把握するのが大変です。

ということで、ここで登場するのが DI Container です!

DI Container は依存性の注入をサポートしてくれるソフトウェアパターンです。

DI Container は、DI 対象のクラスのインスタンスを保持し、指定の変数に生成したインスタンスを代入してくれる機構です。

そしてあともう一つ、大事な概念があります。制御の反転(Inversion of Control)です。

制御の反転 (Inversion of Control) ってなに

前述したサンプルコードでは Applicaiton という実行クラスが DI する役割を持っていました。制御の反転(以下 IoC)は今回の例に当てはめてざっくりいうと、DI する役割をフレームワークに持たせることです。

もう少し具体的にイメージするために Wikipedia から概要を抜粋します。

https://ja.wikipedia.org/wiki/制御の反転

従来式のプログラミングでは、例えば、あるアプリケーションのmain関数が、メニューライブラリ内の関数を呼び出して、利用可能なコマンドの一覧を表示し、その中のどれか一つの機能をユーザーに選ばせる。 一方、制御の反転を使うと、このプログラムは、汎用的な振舞いやグラフィック要素を持っているソフトウェアフレームワークを使って書かれることになるだろう。そうしたフレームワークには、たとえばウィンドウシステムや、メニュー、マウス制御などが既に組み込まれている。個別に開発するコードは、フレームワークの「空白部分を埋める」ものになる。たとえば、メニュー項目の一覧を与えるとか、それぞれのメニュー項目に対応するサブルーチンを登録するといったものだ。

つまり、アプリケーションが main 関数で実行していくのではなく、アプリケーションが実装したサブルーチンを、フレームワークが実行することです。これが「制御を反転させる」意味です。

DI と IoC を組み合わせることでより強力な武器になるわけですね。

おわりに

今回は依存性の注入とその周辺の概念について整理してきました。知っている方にとっては当たり前な内容だったかもしれませんが、少しでもご理解に役立っていると嬉しいです。逆にあまり知らなかった方は、概念的な話が多く具体的なイメージが持ちづらかったかもしれません。

後編の以下の記事ではフレームワークを用いたサンプルコードを掲載し、より具体的な実装に踏み込みますので合わせてご覧ください!

nbtk.hatenablog.com

そして マネーフォワード関西拠点 Advent Calendar 2021 明日は shinya inoue さんです!お楽しみに!

参考にさせていただいた記事

Inversion of Control Containers and the Dependency Injection pattern

Inversion of Control コンテナと Dependency Injection パターン

Service LocatorとDependency InjectionパターンとDI Container - nuits.jp blog