この記事はウェブクルー Advent Calendar 2025 の7日目の記事です。 でした。
昨日は同期の@reon_kunishi_wcさんのJava 25(JEP483)で、Cloud Runのコールドスタートは早くなったのかでした。
同期揃って遅刻して申し訳ございません。
引っ越し直後で段ボールに囲まれながら記事を書いています。
埃っぽいです。
はじめに
下書き段階ではZIOのリソース管理1について書こうと思っていたのですが、筆が進まなかったのでDIについて書くことにしました。
想定読者
- Scalaを書いている方
- Scalaの何かしらのDIツールの使用を検討している方
- Effect-TSでDIしようと思っている方2
Dependency Injection(依存性の注入)とは
Wiki
DIを利用したプログラムを作成する場合、コンポーネント間の関係はインタフェースを用いて記述し、具体的なコンポーネントを指定しない。具体的にどのコンポーネントを利用するかは別のコンポーネントや外部ファイル等を利用することで、コンポーネント間の依存関係を薄くすることができる。
AI
オブジェクトが必要とする依存(他のオブジェクト)を、自分で作るのではなく、外部から渡してもらう設計パターン。
要するに何かの機能を実装する際、必要なものを外部から受け取る方法っていう理解です。
実例
依存が固定されたコード
// ❌ DIなし - 依存が固定されている
class UserService {
private val repository = new UserRepositoryImpl() // 直接newしている
private val emailService = new EmailServiceImpl() // ここも
def register(user: User): Unit = {
repository.save(user)
emailService.sendWelcomeEmail(user)
}
}
DIがない場合の問題点
- テストでモックに差し替えられない
- 設定変更(例:DB接続先)のために本体を書き換える必要がある3
- UserServiceがRepositoryの実装詳細を知っている
個人的には特に1と3が特に辛いと感じます。
依存を外から注入したコード
// ✅ DIあり - 依存を外から渡す
class UserService(
repository: UserRepository, // trait(何らかの抽象とか)
emailService: EmailService // trait(何らかの抽象とか)
) {
def register(user: User): Unit = {
repository.save(user)
emailService.sendWelcomeEmail(user)
}
}
DIのメリット
- テスタビリティ: モックに差し替えられる
- 柔軟性: 環境に応じて実装を切り替えられる
- 疎結合: インターフェースに依存、実装に依存しない
- 単一責任: オブジェクトが自分の仕事に集中できる
手動DIの限界
// 最下層から順番に組み立てる必要がある
val config = ConfigFactory.load()
val db = Database.connect(config.getString("db.url"))
val cache = new RedisCache(config.getString("redis.url"))
val smtpClient = new SmtpClient(
host = config.getString("smtp.host"),
port = config.getInt("smtp.port"),
username = config.getString("smtp.username"),
password = config.getString("smtp.password")
)
val templateEngine = new TemplateEngine(config.getString("template.dir"))
val smsApiClient = new SmsApiClient(config.getString("sms.api.url"))
val smsConfig = SmsConfig(config.getString("sms.api.key"))
val stripeClient = new StripeClient(config.getString("stripe.api.key"))
val logger = new Logger(config.getString("log.level"))
val clock = Clock.systemUTC()
// 組み立てる
val userRepository = new UserRepository(db, cache)
val auditRepository = new AuditRepository(db)
val emailService = new EmailService(smtpClient, templateEngine)
val smsService = new SmsService(smsApiClient, smsConfig)
val notificationService = new NotificationService(emailService, smsService)
val paymentService = new PaymentService(stripeClient, logger)
val auditService = new AuditService(auditRepository, clock)
// やっと目的のサービスが作れる
val userService = new UserService(
userRepository,
emailService,
notificationService,
paymentService,
auditService
)
極端な例かもしれませんがやりたいこと書くまでに用意するもの多すぎて辛いです。
Scalaにおける3つのDIアプローチ
※あくまで筆者が触ったことあるものをあげています。
他にもTageless final4とかCake PatternとかFree MonadとかKleisli5とかetc...
DI周りは掘ると面白いです6
対象のDIフレームワーク
- Google Guice(以下Guice)
- Macwire
- ZLayer
Guice
Google製のOSSのDIライブラリです。7
Javaで使われることも多い印象です。
ScalaのデファクトなフレームワークのPlayに標準でサポート8されているので、Play触ってると何も知らずに使いがちです。(新卒の頃の私がそうでした。)
@Injectアノテーションをリフレクション9で読み取ることによってDIを行います。
コンストラクタインジェクションで使うことが多いと思います。
リフレクションを利用しているので実行時に依存を解決します。
↑世間一般的な呼び方かは分かりませんが、動的DIって呼んでます。
実装
// Guiceのコンストラクタインジェクション
class UserService @Inject()(
repository: UserRepository,
emailService: EmailService
) {
def register(user: User): Unit = {
repository.save(user)
emailService.sendWelcome(user)
}
}
// Moduleで実装をバインド
class AppModule extends AbstractModule {
override def configure(): Unit = {
bind(classOf[UserRepository])
.to(classOf[UserRepositoryImpl])
.in(Scopes.SINGLETON)
bind(classOf[EmailService])
.to(classOf[EmailServiceImpl])
}
}
// Injectorが実行時にインスタンスを作成
val injector = Guice.createInjector(new AppModule)
val userService = injector.getInstance(classOf[UserService])
// → この時点でUserRepositoryとEmailServiceが注入される
Macwire
softwaremill製のOSSです。10
こっちはScala用のライブラリ(のはず)です。
コンストラクタインジェクションで実装します。
マクロを使っているので、コンパイル時にDIされます。
配線が綺麗なのとコンパイル時に不整合を見つけやすい点が個人的にとても好きです。
実装
// Macwire: 明示的なコンストラクタ呼び出し + マクロ
class UserService(repo: UserRepository)
lazy val userRepository = wire[UserRepositoryImpl]
lazy val userService = wire[UserService]
// → コンパイル時にマクロ展開
// 展開後: new UserService(userRepository)
ZIO(ZLayer)
ZIO11はScala 向けの型付けされた効果(Effect12)ライブラリ/ランタイム」で、非同期・並行処理・リソース管理・テスト基盤などを提供します。
ZLayer(Effect-TSでいうLayer)が作られた経緯はScalaのDIだと依存性グラフを表現するのが面倒だったことに端を発するようです。13
ZLayerは型レベルで依存を表現します。
具体的には以下のようなような型をもっています。
ZLayer[Input, E, Output]
ZLayerはここまで紹介した二つに比べかなり高機能です。
単なるDIツールではなく、依存関係を作成するための「設計図」といえます。
ZLayerは依存関係グラフを構築する非同期的で副作用を持ち、リソースを消費するプロセスを記述する型安全なデータ型です。ZLayer[Input, E, Output]は、いくつかのサービスを引数として受け取り、いくつかのサービスを結果として返すレシピであると言えます。
実例
import zio._
// サービスのインタフェース
trait Formatter { def format(s: String): String }
trait Compiler { def compile(s: String): String }
class Editor(formatter: Formatter, compiler: Compiler)
// 単純な提供 Layer(副作用なし)
val formatterLayer: ULayer[Formatter] = ZLayer.succeed(new Formatter { def format(s: String) = s.toUpperCase })
val compilerLayer: ULayer[Compiler] = ZLayer.succeed(new Compiler { def compile(s: String) = s.reverse })
// Editor は Formatter と Compiler を必要とするので fromZIO で作る
val editorLayer: ZLayer[Formatter & Compiler, Nothing, Editor] =
ZLayer.fromZIO {
for {
f <- ZIO.service[Formatter]
c <- ZIO.service[Compiler]
} yield new Editor(f, c)
}
// 合成して使う(提供する側が無い場合は任意の Layer を compose)
val appLayer: ULayer[Editor] = (formatterLayer ++ compilerLayer) >>> editorLayer
// 実行側
val program = ZIO.serviceWithZIO[Editor](editor => ZIO.succeed(println("editor ready")))
program.provideLayer(appLayer)
リソース(DB コネクションなど)は ZLayer.scoped / ZIO.acquireRelease を使って安全に管理できます。
合成演算子:++(並列に複数のサービスを提供)・>>>(左の出力を右の入力に繋ぐ)などを使って依存グラフを組み立てます。
このようにZLayerは単なるDIだけでなく、依存グラフを組み立てたり、リソース管理等も合わせて行うことができます。
便利な反面学習コストは高めだと思います。
比較
| 項目 | Guice | MacWire | ZLayer |
|---|---|---|---|
| 概要 | 実行時 Injector によるリフレクションベースの DI | マクロでコンパイル時にワイヤリングコードを生成する DI | ZIO の型安全な Layer。依存・初期化・資源管理を表現 |
| 依存解決のタイミング | 実行時 | コンパイル時(生成コード) | コンパイル時に型で表現、実行時に provide して初期化 |
| 型安全性 | 中程度(ランタイムでのミスがあり得る) | 高い(コンパイルで検出) | 高い(型パラメータで必要/提供を表現) |
| ランタイムオーバーヘッド | 比較的高い(リフレクション/プロキシ) | 低い(生成コード=通常の new 呼び出し) | 低い(関数合成ベース、軽量) |
| ライフサイクル / リソース管理 | 手動 or コンテナ依存 | 手動(自前で管理) | 組み込み(ZLayer.scoped / acquireRelease) |
| 動的差し替え | 容易(実行時バインド切替可能) | 難しい(ランタイム切替は手間) | テスト差し替え容易(provideLayer で置換) |
| テストしやすさ | テストモジュールや Injector を用意 | 依存を明示して手動で差し替え(シンプル) | Layer を差し替えるだけで簡単にテスト可能 |
| 学習コスト | 低〜中 | 低〜中 | 高(ZIO の概念・型システム習熟が必要) |
| エコシステム / 対象 | Java/Scala の既存エコシステムと親和性高い | Scala 向け(軽量) | ZIO エコシステムと親和性高い(Test/Streams 等) |
| 向いている場面 | ランタイム柔軟性や AOP を活かしたい大規模アプリ | 高性能・型安全を重視する Scala アプリ | 副作用管理・安全なリソース管理・関数型設計を重視する場合 |
| 主な欠点 | 起動コスト・ランタイムエラーの可能性 | ランタイムでの柔軟性が乏しい、マクロ依存 | 学習コスト、Layer を大きくしすぎると型が煩雑 |
最後に
DI面白いですね。
今度はTageless finalやってみたいです。
明日は@wc-terashimaさんの「紙の山」が教えてくれた、検索できることの価値
になります。お楽しみに!
ウェブクルーでは一緒に働いていただける方を随時募集しております。
お気軽にエントリーくださいませ。
https://www.webcrew.co.jp/recruit/
備考欄
-
今回ご紹介するZIOはEffect-TSとかなり似ているので参考になる部分が多いかと思います。 ↩
-
DIに関する技術書などでよく例に出てくるDBの変更にも対応できるみたいなのはあまりしっくりきていないです。(そうそうないと思うので・・・)むしろ外部チャットツールとの連携とかの方が例としてはしっくりくる気がします。 ↩
-
https://speakerdeck.com/aoiroaoino/da-bian-dayo-tagless-final-patan
https://zenn.dev/lanexpr/articles/b4bc4c7b952bae ↩ -
筆者はほぼ理解できていません ↩
-
https://www.playframework.com/documentation/2.7.x/ScalaDependencyInjection ↩
-
https://docs.scala-lang.org/ja/overviews/reflection/overview.html ↩
-
今回はZIO自体の解説はせず、あくまでZIOを使ったDIの方法について簡単に触れます。 ↩
-
https://zio.dev/reference/di/zlayer-constructor-as-a-value ↩