イベントソーシングではテーブルを正規化しない理由

最近、イベントソーシングについて調べていて RDB でテーブルの正規化を行なっている実装例があまりないことに驚きました。そして、正規化をしない理由について、言及している情報があまりなかった。

イベントソーシングでは、イベントストアという発生したイベント情報を記録するテーブルを定義するのが主流であり、ペイロードはjsonblob形式で保存されます。 調査前の無知な自分は、がっつり正規化した下図のような構成を想定していました。

---
config:
    theme: forest
---
erDiagram
    models {
        UUID model_id PK
        DATETIME created_at
    }

    model_events {
        UUID model_event_id PK
        UUID model_id FK
        INT event_no
        DATETIME occured_at
    }

    model_event_A_events {
        UUID model_event_id FK "PKも兼ねる"
        UUID model_invoice_id
        STRING hogehoge
    }

    model_event_B_events {
        UUID model_event_id FK "PKも兼ねる"
        UUID model_invoice_id
        STRING foofoo
        STRING barbar
    }

    models ||--o{ model_events : has
    model_events ||--o{ model_event_A_events : has
    model_events ||--o{ model_event_B_events : has

イベントソーシングで扱いたいモデルを表現するテーブル(models)を定義、さらに対応するイベント群の親テーブル(model_events)を定義します。
そして、イベントそれぞれに対応するテーブル(例: model_event_A_events)をイベントの数だけ定義するシンプルな構成です。この構成では、対象となるモデルの実態が存在することがmodelsを見れば分かるのと、イベントの拡張に対して、テーブルを追加するだけで対応できる長所があります。

例: 銀行口座のイベントソーシング

  • accounts
  • account_events
  • (入金)acount_event_deposit_events
  • (引出)acount_event_withdrawal_events

結論

結論としては「正規化をする強い理由がない」に至りました。
個人的には特に理由がなければ RDB を使った方が良いと思いますが、求められる要件・性能によっては NoSQL などの選択はありえます。

観点

比較対象とするイベントストア。
Building an Event Storage | CQRS で紹介されているテーブル定義です。

---
config:
    theme: forest
---
erDiagram
    aggregates {
        UUID id PK
        string type
        int version
        datetime created_at
        datetime updated_at
    }

    events {
        UUID id PK
        UUID aggregate_id FK
        int version
        string type
        json payload
        datetime created_at
    }

    aggregates ||--o{ events : has

一度、発生したイベントは更新しない

イベントソーシングではイベントは一度、発生したら更新・削除することは推奨されません。 先のイベントを取り消したい場合、新たに取り消し用のイベントを発生させます。つまり、データの挿入はあれど、更新(編集・削除)はないため、正規化によるデータ整合性がメリットになり得ません

aggregateseventsでは外部キー制約を持つ点と、他のテーブルは必ずしもイベントソーシングを用いるかは分からないため、RDB を使いつつjsonblobを用いるという方針はしっくりきました。

読み込みが圧倒的に多い

アプリケーション特性にもよりますが、多くの場合、データは書き込み < 読み込みとなります。
イベントソーシングでは、先ほどのような正規化を行うとイベントの数だけJOINが必要となる対象のテーブルが増えるため、パフォーマンスが悪化していきます。マテリアライズド・ビューやスナップショットを活用する方法は考えられますが、いずれも正規化をしない設計にパフォーマンス・コストが劣ることは確定です。

-- 対象モデルのイベントを取得する
SELECT
  models.model_id,
  model_events.occured_at,
  CASE
    WHEN model_event_A_events.model_event_id IS NOT NULL THEN 'A-Event'
    WHEN model_event_B_events.model_event_id IS NOT NULL THEN 'B-Event'
    ELSE 'Unknown'
  END AS event_type,
  model_event_A_events.*,
  model_event_B_events.*
FROM
  models
  JOIN model_events ON models.id = model_events.model_id
  JOIN model_event_A_events ON model_events.id = model_event_A_events.model_event_id
  JOIN model_event_B_events ON model_events.id = model_event_B_events.model_event_id
WHERE
  models.model_id = 'some-uuid-value'
;

正規化をするメリットがない以上、パフォーマンスを悪化させる設計を選ぶ必要はありません。

複雑さとスキーマ定義

データ復元(読み込み)と書き込み時の複雑さは、テーブルのスキーマ定義に依存します。 ペイロードをjsonで扱うと、本当にデータが登録されているのか、期待する型のデータとして復元できるのかを保証することが難しくなります。
アプリケーション側(Repositoryの実装クラスなど)で、読み込み時のフィールド存在チェック・書き込み時のバリデーションなど、複雑さを吸収することになるでしょう。

RDB でスキーマ定義を丁寧に行えば、その限りではありませんが、読み込み時のパフォーマンスを考慮するとイベント単位でテーブルを分割することは避けたいです。しかし、1つのテーブルで複数イベントの構造を表現しようとすると、存在しうるフィールドを全てnullableとして定義する必要があるため、結果的にスキーマの見通しが悪くなります。

event_id event_name version occured_at aggregate_id hoge foo bar piyo
1001 EventA 1 2025-08-31 09:00:00 2001 NULL orange NULL NULL
1002 EventB 2 2025-08-31 09:05:00 2001 100 apple NULL xyz
1003 EventC 3 2025-08-31 09:10:00 2001 NULL foo1 bar1 NULL

多少、アプリケーション側の実装が複雑になってもjsonblobを使う価値はありそうです。

データ分析

jsonblobのフィールドに対してのクエリは、標準 SQL として定義されておらず RDB によってサポートされている構文・機能が異なります。 たとえば、ユーザーがどういった操作を頻繁にしているかを分析したいという要求が考えられそうですが、正規化されていないテーブルを SQL で分析するのは大変です。

とはいえ、必ずしも SQLだけで完結する必要はない(エクスポートして前処理をする)のと、分析用のdbtにデータを同期するといったアプローチも検討できるため、正規化をするかどうかの大きな理由にはなりません。

参考文献

GraphQLのQueryはどの単位で定義すれば良いのか

長らく REST API を活用してきたため、実務で GraphQL の経験がありませんでした。 特に困ったのが Query をどの単位で定義すれば良いのか?という点です。簡単そうな問題に感じますが、執筆前の自分では言語化するのが難しい状態でした。

色々と GraphQL について調べた結果、自分なりに納得できて方針が見えてきたので、備忘録としてこの記事を書いています。

リソースを元に考える

REST API では/users/postsのようにリソース単位でエンドポイントを定義します。 GraphQL でも同じように Query を定義しますが、リソースを元に考えるという観点では REST API と GraphQL に大きな違いはありません。

※ ここでいうリソースというのは、実際にはドメインモデルや DB のレコードに対応するオブジェクトなど

type Query {
  users: [User!]!
}

query GetUsers  {
  users {
    id
    name
  }
}

この時点では、REST API と比較して GraphQL では取得するデータをフィールド単位で取捨選択できる(データ取得のオーバーヘッドを削減できる)という点での優位性しかありません。 自分の考えでは、膨大な数のリクエストが来るような大規模なサービスを除き、あえて GraphQL を採用する大きな理由にはなりません。

ユースケースはクライアントが定義する

GraphQL の強みはqueryを組み合わせて、さまざまなユースケースをクライアントで自由に定義できる点です。 GraphQL では/graphqlという単一のエンドポイントでリクエストを受け付けるため、リクエスト(.query)の書き方を変更するだけで、直接には関連をもたないリソースであっても一度のリクエストで取得することができます。

こういったリソースを組み合わせれば解決するケースでは、専用のQueryをわざわざ定義するのは避けるべきです。

type Query {
  users: [User!]!
  settings: [Setting!]!
}

query GetUsersAndSettings {
  users {
    id
    name
    age
  }

  settings {
    id
    contents {
      enabled
      label
    }
  }
}

REST API の場合、シンプルさは魅力ですが、思いつくアプローチはどれも微妙です。

  • 新たなエンドポイントを定義する: ユースケースが増える度に対応が必要
  • クエリ文字列で指定する: APIの複雑化
  • /usersのレスポンスにsettingsを含める: データのオーバーフェッチ
  • bffを使う: アーキテクチャの複雑化
    • backend for frontend: データ集約を行うレイヤーを挟むアーキテクチャ

GraphQL はこの問題を見事に解決しています。
新たにクライアントで定義したリクエスト(.query)は不要になれば、捨てれば良いです。フィールドに対する Resolver などの拡張は必要ですが、ユースケースの拡張に合わせて必ずしもサーバー側の変更は必要になりません。

一方、GraphQL のトレードオフにはキャッシュ・認証の複雑化、思わぬ負荷(N+1が発生しやすい)がよく挙げられるため、注意が必要です。

複雑なユースケースはどう定義する?

前提 GraphQLはCQRS

GraphQL はデータの取得(Query)と変更(Mutation)が分離されており、いわゆる CQRS(Command Query Responsibility Segregation)のパターンに該当します。ユースケースに合わせてデータの取得方法をクライアントが定義できる点はまさに CQRS です。
しかし、backend で CQRS を扱い際に利用することが多いQuery Serviceほどの柔軟性さはなく、実際にどのような経路でデータが取得されるかは知りません(データとの物理的な距離による制約)。

リソースの組み合わせで解決できないなら、専用Queryを作るしかない

クライアントのリクエストだけで完結できない複雑なユースケースの場合、どうすれば良いでしょうか。 よくあるのは、月間のレポートを表示するといった集計処理が必要になるようなケースです。クライアント側で関連するデータなどを全件、取得して、集計処理を行うのはリソース効率の観点で考えると悪手といえます。

不必要に集計ロジックがフロントに露出してしまう点も微妙...

const query = gql`
  query GetUserActivities {
    users {
      id
      activity: {
        kind
        timestamp
      }
    }
  }
`;
const res = client.request(query);
const aggregated = res.data.users.reduce((user, accum) => { ... })

複雑なユースケースの解決策として、リソースの組み合わせで表現できない・しにくい場合は専用の Query を定義することが考えられます。 重要なのはリソースの組み合わせでは表現できない・しにくい場合のみということ。基本的にはリソースを元にクライアント主導で考えるべきで、新たな Query として切り出すのは最終手段です。 次々に専用 Query として切り出してしまうと、GraphQL の強みを生かせず管理コストが爆増します。

元となるリソースが存在しないユースケース

先ほどの複雑なユースケース(集計処理)は別の捉え方ができます。
集計結果というのはドメインモデルとして定義されているわけでもない、あるデータから変換して作られるデータです。つまり、ある Query の結果として表現されるデータであり、元となるリソースが存在しないため、別のQueryとして切り出せば良いと考えられます。

まとめ

  • 常にリソースを元に考える
  • クライアントでリソースを組み合わせて、さまざまなユースケースを定義する
  • 複雑なユースケースの場合、専用のQueryを定義することを検討する

参考文献

開発環境2025 summer

最近、色々とツール周りを更新したので、自分の開発環境を以下の記事に習って記録しておきます。 生産性への投資は本当に大事。後回しになりがちなので、しっかりと改善していきたい所 ...

blog-dry.com

各種ツールの設定は dotfiles として管理しているので、参考までに。

エディタ

NeovimVSCode(+ vscode-neovim)の二刀流です。
業務でKotlinを書くため、 IntelliJ IDEA も使っていますが、特にこだわりはないので詳細は省略します。

最近はTypeScriptを書くことが多く、ちょっとしたコーディングは Neovim で完結させることが多いですが、メインでは VSCode(+ vscode-neovim)を使っています。 NeoVim の開発効率を取り入れたい一方、Copilot なんかを Neovim に設定してまで使いたいモチベーションがないのと、ペアプロで LiveShare が使えないと困るケースが多いため、VSCode でスムーズな開発ができるようにメインエディタとして整えています。

vscode-neovim と Neovim の設定ファイルは共通化できますが、自分はあえて分けています。
捨てやすさを優先しての選択です。Neovim の設定ファイルはinit.vimになっている一方、vscode-neovimの設定ファイルはinit.luaになっているので、.luaに統一したい ...

(2025/07/19追記: 記事公開後に.luaに更新しました)

ターミナルエミュレーター

iterm2 を長らく愛用していましたが、Ghostty に乗り換えました。
特に iterm2 に不満があったわけではないですが、Zero Configuration Philosophy に惹かれました。
設定がシンプルであればあるほど良いと考えているのですが、実際に設定ファイルを書いてみて、記述量の少なさに驚きました。設定ファイルはプレーンなテキストファイルなので、git で管理できるのも嬉しいです。

自分の環境(M1・M3 Mac)ではかなりサクサクと動いています。軽量を売りにしているのも納得です。

ターミナルマルチプレクサ

相変わらず tmux を使っています。実は本格的に使い出したのは去年の夏頃です。
キーバインドに若干の扱いにくさはありますが、慣れれば問題なかったです。合わせて、開発環境を一発で立ち上げるのに tmuxinator を使っています。

ymlファイルを定義しておけば、指定のウィンドウ・ペインなどをコマンド1つで立ち上げられるのが本当にありがたい。

シェル

zshを使っています。
特にこだわりはないですが、Mac のデフォルトシェルになった頃から bash から乗り換えました。

ランチャー

Raycast を使っています。 今までランチャーを使ってこなかったんですが、生産性が爆上がりしたので、絶対使った方が良いです。 Snipet がめちゃくちゃ便利。テストデータに保険者番号と呼ばれる特定の番号を入力する必要が頻繁にありますが、,hokenと入力すれば、自動で変換してくれます。

バージョン管理

asdf から mise に乗り換えました。 不満は特になかったですが、mise は環境変数・タスクの管理まで可能です。タスクに tmuxinator のコマンドを登録して、入力の手間を省いています。

Docker container launcher

OrbStackを使っています。とにかく速い。UIがシンプルで分かりやすいです。

ウィンドウマネージャー

Rectangle(シンプルで良い)

ブラウザ

Google Chrome(ずっと使ってる。重いのがね ...)

キーボード:

Mac Book 標準のキーボードです。キーボードにこだわりないです。
過去に HHKB と NiZ を試しましたが、しっくりこなかった。どこでも仕事ができるように Mac Book だけで仕事ができるようにしておきたい思いがあります。

モニター

BenQ GW2785TC をずっと使っています。
2年ほど使っていますが、type-c 一本で出力・給電できるのは超便利です。特に不満はないですが、そろそろ買い替えても良いかも。最近はフロントを触ることが多いので、もう少し大きいサイズにしたい気持ちもあります。

作業速度を上げ続ける

この記事は株式会社ヘンリー - Qiita Advent Calendar 2024の4日目の記事です。
先日はid:take7010さんの「採用の時に見るべきポイント 面接偏」でした。

今回は「作業速度を上げ続ける」をテーマに書きます。
ちょっとした作業を早く完了できるかは、1つ1つは小さなことですが、日々の積み重ねによって結果的に膨大な時間の差が生まれます。 良いエンジニアは、そういった時間を生み出し、もっと重要なことに時間を使っています。

では、どうすれば作業速度を上げられるかというと、一言で言えば「道具をこだわる・知る・磨く」に尽きます。
例えば、普段使いのエディタの機能・ショートカットを覚えるとか、そもそもエディタをより良いものに変えるとかですね。 ソフトでもハードでも何でも良くて、作業の中で触れるもの全てが対象です。僕は高級キーボードと呼ばれるものを色々試してみましたが、特に強いこだわりがないことが分かり作業速度の改善を感じることができませんでした。

今回は考え方や精神論、モチベーションといったものは置いておいて、実際に道具を変えた・改善した結果、変化を感じられているものを列挙していきます。

エディタを変えた

エディタを長年使用していたVSCodeからNeovimに乗り換えました。
しかし、普段使いのエディタはNeovimに乗り換えましたが、業務ではIntelliJ IDEAをメインに使っています。
ヘンリーではバックエンドの開発言語にKotlinを採用しており、Kotlinの公式LSPは一般公開されておらずjetbrains製のIntelliJ IDEAを使うのがデファクトなためです。

Neovimに乗り換えた理由はいくつかあります。

  • 1️⃣ 使い慣れた場合の速度感への期待
  • 2️⃣ ターミナルで作業を完了させたい
  • 3️⃣ カスタマイズ性の高さ

最初は不慣れなキーバインドに苦戦しましたが、1ヶ月を過ぎた頃にはVSCodeでは得られなかった圧倒的な速度を感じられるようになり、期待通りの結果を得られました。 Neovimに乗り換えて感じた一番のメリットは、とにかくカーソル移動が秀逸な点です。コードは読んでいる時間の方が長いので、大きなアドバンテージになります。

tmuxを使う

Neovimに乗り換えた理由の「ターミナルで作業を完了させたい」と同じ動機です。
以前はターミナルの画面を垂直分割して、複数サーバーの立ち上げなんかを画面カツカツの状態で行っていましたが、tmuxを使うことでスマートに画面(セッション)を切り替えられる上、自由に配置することが可能です。 何よりNeovimとの相性も良く、ターミナルへの引き篭もりが捗ります。

zshへの完全移行

長らく.bashrcで管理していたアレコレを.zshrcへ完全移行しました。
cdrの有効化や、zsh-syntax-highlightingを使ってシンタックスハイライトを有効化したりしました。特に気に入っているのはCtrl+rで実行可能なコマンド履歴の検索をpecoでインタラクティブに行えるように変更した点です。

以前はhistory | grep XXXXしたりデフォルトのコマンド履歴検索(Ctrl+r)を愚直に使っていたので、かなり快適になりました。 今思うと無駄な時間を過ごしていたなと感じます。

qiita.com

原始的な道具を知る・使う

これまでに紹介したNeovim(正しくは前身のvimやvi)やシェルも原始的な道具の1つといえます。
原始的な道具を使うことのメリットは何よりも形骸化しにくく、環境に依存しにくくポータブルな点です。

改善を続ける

ここまで紹介したものは全てファイルで設定が可能で、自分は変更をgitで管理しています。
どれも現時点のマイベストでしかなく、もっと作業が早くなる設定や新たなツールが今後、生まれる可能性があります。
今後も自身の作業速度を上げ続けるために継続的に改善・管理していきたいので、dotfilesについての紹介は省きますが、自身のdotfilesを育てはじめました。現状、mac以外での動作を期待していないのと、依存しているツールなどの自動インストールには対応していません。必要になったら足していきます。

github.com

以上です。
明日の株式会社ヘンリー - Qiita Advent Calendar 2024id:Songmuさんです。お楽しみに。

明日からDIできちゃうKoin入門

RubyOnRailsからやってきた自分にとってKotlinやJavaで、しばしば行われるDI(Dependency Injection: 依存性の注入)は一般的なものではなく、名前は聞いたことがあるけど使ったことはあまりないというものでした。現職ではKoinというKotlin向けのDI用のライブラリを使っているのですが、初めはどこでレポジトリの実装クラスのインスタンスを作ってるんだ...と混乱したものです。今でもKoinの使い方・仕組みがよく分かっていなかったので、実際にコードを動かしながら理解を進めてみました。

この記事は自身のメモ兼、最低限の理解をして明日からKoinへ入門できることを目指して書いています。
意外にもbuild.gradle.ktsで書かれたプロジェクトのサンプルコードを見つけることができず、一からプロジェクトをセッティングしたのですが、かなり時間がかかりました。Gradleについての無知を反省しつつ動くKoinのサンプルコードとして成果物をGitHubに公開しているので、こちらも合わせてご覧ください。

github.com

Koinの使い方

まずKoinではmoduleというものを定義することで、DIされる・する側の関係性を定義します。
後に登場するKoin AnnotationsModuleを簡潔に定義するためのメタデータをクラスに定義するための、補助的なツール群です。 まずはシンプルにmoduleを定義するところから始めてみます。

注入するクラスの用意

まずは簡単な医薬品に関するデータクラスとレポジトリのinterfaceと実装クラスを用意しておきます。 余談ですが、医薬品を選んだのは普段、医療ドメインの開発に関わっているので、少し馴染みがあるもの選択してみました。

data class Medicine(val name: String, val price: Int)

interface MedicineRepository {
    fun findByName(name: String): Medicine?
    fun save(medicine: Medicine): Medicine
}

class MedicineRepositoryImpl : MedicineRepository {
    var medicines = mutableListOf<Medicine>(
        Medicine("解熱剤", 80),
        Medicine("頭痛薬", 120),
        Medicine("咳止め", 160),
    )
    override fun findByName(name: String): Medicine? = medicines.find { it.name == name }

    override fun save(medicine: Medicine): Medicine {
        medicines.add(medicine)
        return medicine
    }
}

次にレポジトリを使用して解熱剤を取得するサービスクラスを定義します。
今回は医療品の一覧mutableListOfで定義していますが、型定義上では医薬品が見つからない可能性があるため、該当する医薬品が見つからなかった場合に例外を投げるようにしました。

class MedicineService(private val medicineRepository: MedicineRepository) {
    fun get解熱剤(): Medicine {
        val 解熱剤 = medicineRepository.findByName("解熱剤")
        return 解熱剤 ?: error("解熱剤が見つかりませんでした")
    }
}

moduleとKoinComponent

残りはmoduleKoinComponentを継承したクラスを定義です。
以下のようにsingleとレシーバーを用いて関係性を定義することでMedicineRepositoryを引数に持つクラスに対してMedicineRepositoryImplが注入されるようになります。 get()singleのレシーバー内で使用することができる、よしなに値の注入を行うための関数です。注入したい値の数が複数ある場合にはget(), get()...のように記述すれば良いです。

val appModule = module {
    single<MedicineRepository> { MedicineRepositoryImpl() }
    single { MedicineService(get()) }
}

最後にKoinComponentを実装したクラスで注入される値を定義します。
登場人物が多くなってきてややこしくなってきましたがby inject()を用いることでMedicineServiceが注入されます。 Applicationは単に注入されたクラスをgetterで参照するだけのシンプルな実装にしてあります。これだけだと「何のためにApplicationを定義してるの?」と思われるかもしれませんがKoinで値を注入させるためにはKoinComponentを継承したクラスを定義する必要があるので、必要なステップです。

class Application : KoinComponent {
    private val _medicineService: MedicineService by inject()

    val medicineService get() = _medicineService
}

動作確認

これでDIの準備が整いました。 上記で定義したappModulestartKoinのレシーバー内でmodulesの引数に指定します。

fun main() {
    startKoin { modules(appModule) }

    val medicine = Application().medicineService.get解熱剤()
    println("薬の情報: ${medicine.name}${medicine.price}円です")
}

./gradlew runで実行してみます。
無事にDIされて医薬品の情報が表示されていることが確認できました。

$ ./gradlew run

> Task :run
薬の情報: 解熱剤は80円です

今まで定義したものを図にしてみると、こんな感じでしょうか。
やはり登場人物は多いなと感じますが一度、枠組みを作ってしまえばレポジトリやサービスクラスの追加は簡単にできそうです。

Koin Annotations

先ほどの例では登場人物が多くて関係性を定義しないといけなかったりと、手間に感じる点がありました。
そのためKoinではより手軽にDIが行えるようにKoin Annotationsというツールを提供しています。

今回は以下3つのAnnotationsを使ってDIしてみます。

  • @Module
  • @ComponentScan
  • @Single

@Moduleと@ComponentScan

先ほどは関係性を手動で定義しましたが@Module@ComponentScanを使うことで、特定のパッケージ配下に定義されたクラスを動的にmoduleに追加するということが可能になります。上手くパッケージングされていることが前提となりますが、作業が大幅に減るため、上手く活用したい機能です。

@Module
@ComponentScan("org.example.medicine")
class AppModule

これでorg.example.medicine配下のクラスを動的にmoduleとして定義してくれます。
「ん?DIの対象になるクラスはどうやって決まるの?」という疑問が浮かびますが、そこで登場するのが@Singleです。

@Single

使い方は超簡単でDIの対象としたいクラスの上部に@Singleと記述するだけです。
今回の例で対象となるクラスはMedicineRepositoryImplMedicineServiceです。どちらもmoduleの定義でsingleとして定義したクラスですね。

@Single
class MedicineRepositoryImpl : MedicineRepository {
  :
}

@Single
class MedicineService(private val medicineRepository: MedicineRepository) {
  :
}

動作確認

ApplicationクラスとstartKoinの記述は同様に行う必要があります。
ただしKotlin Annotationsではbuild時に生成されたコードを使用するため、ファイル上部でorg.koin.ksp.generated.*importしないとエラーになります。build.gradle.ktsに設定を追記しないとimportできない...という罠があります。

sourceSets.main {
    java.srcDir("build/generated/ksp/main/kotlin")
}
import org.koin.ksp.generated.*

fun main() {
    startKoin {
        modules(AppModule().module)
    }

    val medicine = Application().medicineService.get解熱剤()
    println("薬の情報: ${medicine.name}${medicine.price}円です")
}

結果については同じなので省略します。

まとめ

  • KoinはDIを行うためのフレームワーク
  • Koinではmoduleを用いて注入する・される側の関係性を定義する
  • DIを行うためにはKoinComponentを実装したクラスを定義する
  • startKoinにてmodulesの引数に定義したmoudleを指定する
  • KoinではDIをより簡単に行うためにAnnotationsというツールを提供している
  • @Moduleと@ComponentScanを用いることで指定パッケージ配下から動的にmoduleを定義する
  • @Singleが付与されたクラスがDIの対象となる

今回記述した意外にもAnnotationsには種類があるようです。
詳しくは公式のドキュメントを見てみてください。

insert-koin.io

参考文献