こんにちは。技術本部 Strategic Products Engineering Unit Contract One Devグループの髙野です。2024年4月に新卒でSansan株式会社へ入社し、契約データベース「Contract One」の開発をしています。5人チームのリーダーとして、新規機能開発に向き合っています。
Contract Oneでは長らくKotlin 1.7.21を使用してきましたが、社内ハッカソンでKotlin 2.1へのアップデートを行ったので、紹介します。
Kotlin 2.1へのアップデートのモチベーション
K2 Compilerによるパフォーマンスの向上
今まではCI上でのコンパイルに10分程度かかっていて、テストが回り切るのも合わせると合計で20分弱かかっていました。K2 Compilerによりコンパイル時間が低減され、ローカルやCIでのアプリ・テストの実行にかかる時間の減少が期待されます。
また、スマートキャストの強化により、さらなる型安全な記述が可能になります。
fun testString() { var stringInput: String? = null // stringInputはStringとしてスマートキャストされる stringInput = "" try { // コンパイラはstringInputがnullではないことを知っている println(stringInput.length) // 0 // コンパイラは前のスマートキャスト情報を使わなくなり、 // ここからはString?にスマートキャストされる stringInput = null // Exceptionを投げる if (2 > 1) throw Exception() stringInput = "" } catch (exception: Exception) { // Kotlin 2.0.0では、コンパイラはstringInputが // nullの可能性があることを知っているので、nullableとして扱う println(stringInput?.length) // null // Kotlin 1.9.20では ? operatorが不必要だが、これは間違い } }
private constructorなdata classのcopyメソッドの可視性の変更
Contract Oneはレイヤードアーキテクチャ(DDD)を採用しており、ドメイン層にドメインロジックが集められています。例えば、10文字以内のタイトルを持つ、Contract
Value-Objectがあったとします。
data class Contract private constructor( val title: String ) { companion object { fun new(title: String): Contract { require(title.length <= 10) return Contract(title = title) } } }
このコードでは、newメソッドでのみインスタンス化できるようにconstructorの可視性がprivateになっています。しかし、copyメソッドを使うことによって、意図しないインスタンスを生成できてしまいます。
// Pass val contract = Contract.new(title = "契約書タイトル") // NG: requireに引っかかるのでthrowする val unreachableContract = Contract.new(title = "10文字以上の契約書タイトル") // Pass (Oops!) val invalidContract = contract.copy(title = "10文字以上の契約書タイトル")
Kotlin 2.1から、private constructorなdata classでcopyメソッドを使用している場合は、warningが表示されるようになります。Kotlin 2.2からは、copyメソッドの可視性がconstructorと同じになり、上記のコードはコンパイルエラーになります。これにより、不適切にdata classをcopyして使用することを防止できます。
アップデート作業
Kotlin 1.7.21からKotlin 2.1に上げるに当たって、非推奨で削除されたものだけは置き換えが必要でしたが、それ以外のコードの変更は不要でした。
Kotlinのバージョンを上げる
Version Catalogを使用しているため、settings.gradle.kts
でバージョンを変更します。
dependencyResolutionManagement { versionCatalogs { create("libs") { // Before version("kotlin", "1.7.21") // After version("kotlin", "2.1.0") } } }
文字列のcase変換の修正
Kotlin 1.5からtoLowerCase()
とtoUpperCase()
がdeprecatedとマークされ警告が出ていましたが、Kotlin 2.1からは警告ではなくエラーが出るようになりました。これらはLocaleに気をつけながらlowercase()
やuppercase()
に移行します。
// Before val hoge = "Hogehoge".toLowerCase() // After1. Beforeと全く同じ挙動をする書き方 val hoge = "Hogehoge".lowercase(locale = Locale.getDefault()) // After2. 内部でLocale.ROOTが使用される書き方 // Before・After1とは挙動が変わるかもしれないことに注意 val fuga = "Hogehoge".lowercase()
コンパイラオプションの指定方法の修正
Kotlin 2.0から? compilerOptions
DSLが登場し、kotlinOptions
DSLが非推奨となりました。エラーにはならなかったのでアップデートのブロッカーではありませんでしたが、対応しました。
tasks.withType<KotlinCompile> { // Before kotlinOptions { jvmTarget = "17" freeCompilerArgs = "hogehoge" } // After compilerOptions { jvmTarget.set(JvmTarget.JVM_17) freeCompilerArgs = "hogehoge" } }
さいごに
大きな問題が起きることなく、Kotlin 2.1へアップデートできました。
ハッカソンで別のチームが行った「古いアーキテクチャで書かれているE2Eテストをすべて消す」の効果も相まって、20分弱かかっていたCIが12分程度で回り切るようになったほか、ローカル環境でのビルドが体感できるほどにサクサクになりました。
Contract Oneでは、既存のコードの改善や新規機能の開発をさらに加速させるために仲間を募集しています! CIの実行時間を10分以内にするぞという意気込みのある方も募集中です。カジュアル面談など詳しくは採用情報をご確認ください。