この記事はウェブクルー Advent Calendar 2025 の 21 日目の記事です。
昨日は @wc-nakagomi さんの《「ゲームは遊び」なんて言わせない!? 『エルデンリング ナイトレイン』と仕事の意外な共通点》でした。
はじめに
突然ですが、Java から Scala を参照したくなりました。
背景
Webcrew のバックエンド開発言語は元々 Java でしたが、2010 年代後半頃に変化が訪れ、新規サービスの開発時には基本的に Scala を採用する形となりました。
Scala は Java の膨大なエコシステムも活かしながら、Java よりも表現力が高く、安全で快適な開発体験をもたらしてくれる言語です。実際の所、複雑な業務ロジックを扱う上で、その恩恵を受けた場面は少なくありません。
その一方で、運用を続ける中でいくつかの課題も見えてきました。豊富なシンタックスや高度な型システムから生じる学習コストの高さや、即戦力人材が採用市場で希少になっている点などが挙げられます。
また近年では、Java の言語仕様やリリースサイクルも徐々に見直され、当時感じていたペインが相対的に小さくなりつつあります。
こうした状況を踏まえ、新規サービスの開発時の言語選択肢に、あらためて Java を含めてもよいのではとも思っています。
※ちなみに、学習コストが高いと述べてしまいましたが、致命的に高いという実感はありません。社内でも Java の有識者であれば半月から一ヶ月程度で実務をこなせるようになっています。あくまで Java と比較した場合には……という程度です。
——という事で、長々と背景を綴りましたが。
Scala 製の既存業務ロジックを活かしつつ、新規に Java の BackendAPI を立ち上げるとなった場合のシナリオを想定して、Java から Scala を呼び出す事にトライしてみます。
検証
今回は、以下のような構成が実現できるか試してみたいと思います。
.
|-- app ... [新規] Java (SpringBoot)
|-- app-scala ... [既存] Scala
`-- build-logic ... ビルド設定
- 新規のSpringBootプロジェクトと既存の業務ロジックでプロジェクトを分割
- ビルドは Gradle を使用し、Composite Builds によるマルチプロジェクト構成とする
この構成で、app の SpringBoot で実装された Controller から Scala の業務ロジックを参照・利用できる事をゴールとします。
※余談ですが、Gradle でのマルチプロジェクト構成については今回取り上げませんが、これが若干ややこしかったです。提供されている手法自体が何パターンかあるのも理由ですが、時代によってベストプラクティス的なものが微妙に移ろいでいるようで、文献が分散気味になっている所が辛い感じでした。
1. 愚直に参照してみる
近年、Webcrew の Scala 開発では ZIO が採用されています。つまり、Java からみた Scala 業務ロジック側のエントリーポイント(参照メソッド)は ZIO 型を返却します。
trait ExampleUsecase {
def handle(): IO[String, Int]
}
Java 側で、この IO[String, Int] を実行できるのかという問題があります。
@RestController
class ExampleEndpoint {
private final ExampleUsecase usecase;
public ExampleEndpoint(ExampleUsecase usecase) {
this.usecase = usecase;
}
@GetMapping("/message")
String message() {
var zio = usecase.handle(); // ZIO<Object, String, Int>
// zio を実行する方法...?
}
}
という事で、即挫折です。
仮に百歩譲って実行する手段が用意出来たとしても、そもそも ZIO 型自体を操作する事が困難なので、生の ZIO 型が返却された所でエラーハンドリングなどを実装する事が出来ないですね……。
2. Java側の都合にあわせる
となると、Scala 側で Java にとって都合の良い形 でエントリーポイントを公開してあげる必要がありそうです。
そこで Java に参照しやすい形で公開する Facase 層を用意してみました。
trait Facade {
import zio.*
extension [A](zio: Task[A])(using runtime: Runtime[?]) {
def sync: A =
Unsafe.unsafely(runtime.unsafe.run(zio).getOrThrowFiberFailure())
}
}
この Facade 層は ZIO を同期実行した上で、その実行結果を返却する事を責務とします。その為のユーティリティメソッドもあわせて用意しました。
この Facade トレイトを用いて、ExampleUsecase の Facade を実装します。
class ExampleUsecaseFacade(
usecase: ExampleUsecase, ctx: FacadeContext
) extends Facade {
given Runtime[?] = ctx.runtime
def handle(): Int = {
usecase.handle().mapError(e => new RuntimeException(e.toString)).sync
}
}
class FacadeContext {
val runtime = Runtime.default
}
欠点としては、実行対象を Task[A] とする必要がある為、エラー型が欠落する形になってしまいます。Java 側でエラーの種別に応じてハンドリングを制御したい場合は、エラー原因毎に例外を分ける必要も出てくるでしょう。
FacadeContext は Facade 層に Runtime を提供する為だけの Provider です。
上記の ExampleUsecase も SpringBoot で DI される事になる訳ですが、Runtime を DI しやすくする為にこのような形をとっています。
とりあえずはこの ExampleUsecaseFacade があれば、エントリーポイントの返却値が IO[String, Int] から Int に変わる為、Java 側で扱いやすくなります。
@RestController
class ExampleEndpoint {
private final ExampleUsecaseFacade usecase;
public ExampleEndpoint(ExampleUsecaseFacade usecase) {
this.usecase = usecase;
}
@GetMapping("/message")
String message() {
var result = usecase.handle(); // Int
...
}
}
DI の設定も忘れずに。
@Configuration
class AppConfig {
@Bean
public FacadeContext facadeContext() {
return new FacadeContext();
}
@Bean
public ExampleUsecaseFacade exampleUsecaseFacade(
ExampleUsecase usecase,
FacadeContext ctx
) {
return new ExampleUsecaseFacade(usecase, ctx);
}
}
めでたし、めでたし……。でしょうか?
3. Scalaの型をJava側で扱いやすい形に変換する
さて、上記の Facade 層を用意する事で、Java 側で Scala の業務ロジックを参照できるようになりました。しかし、まだ問題があります。
例えば、元の実装の成功値が IO[String, Seq[Int]] 等というように Seq や Map、Set といったコレクション型になっている場合です。これも Facade が Seq[Int] として返却するようにすれば良いのですが、Java 側で Scala のコレクションを扱う事は困難です。
というのも、Scala のコレクション型は scala.collection.Seq, scala.collection.Map, scala.collection.Set として固有に実装されており、Java の標準コレクション型と互換性がありません。
そういう訳で、Scala のコレクション型も Java 側あわせて変換してあげると良さそうです。
import scala.jdk.CollectionConverters.*
class ExampleUsecaseFacade(
usecase: ExampleUsecase, ctx: FacadeContext
) extends Facade {
given Runtime[?] = ctx.runtime
def handle(): java.util.List[Int] = {
usecase
.handle()
.mapBoth(e => new RuntimeException(e.toString), _.asJava) // Task[java.util.List[Int]]
.sync
}
}
これで、Java 側では List<Int> として扱う事が出来るようになります。
この他にも Option 型や Either 型なども Scala 固有の型として実装されている為、Java 側で扱いやすい形に変換してあげる必要があります。
ここまでやると、ようやく Java 側で Scala のロジックを参照できるようになります。
Option 型は scala.jdk.OptionConverters.* を利用すると java.util.Optional に変換する事が出来ます。
まとめ
既存資産を効率よく取り込む方法として、Scalaの実装を新規プロジェクトである Spring Boot に内包させる方法をトライしてみました。
……が、とりあえずやってみたはいいものの、既存のScala/ZIO実装をJava側から安全かつ扱いやすく参照する為には、色々と手を入れる手間がでてしまう事がわかりました。結果として、本来であれば不要なJava側に参照させる為だけの「グルーコード」が増えてしまい、構造が複雑化していく感覚がありました。
既存資産を活かすという目的に対し、追加されるコストが見合っているようにはあまり感じられません。正直筋の良い方法ではないなという印象が強いです。
このケースにおいては、新規プロジェクトを既存プロジェクトと別に立ち上げてネットワークレイヤーで新旧のルーティングを分けながら段階的に移行する方が良さそうです。
Scala から Java を参照するケースは多々ありますが、その逆はあまり実体験がなかったので、実際にやってみて課題感も含めて色々納得することができました。
明日は @yo-shibata さんの投稿です。よろしくお願いします。
ウェブクルーでは一緒に働いていただける方を募集しております。
少しでもご興味のある方は、ぜひお気軽にご連絡ください!🙇
https://www.webcrew.co.jp/recruit/