Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

Contract OneのKotlinをバージョン2.1へアップデートしました

こんにちは。技術本部 Strategic Products Engineering Unit Contract One Devグループの髙野です。2024年4月に新卒でSansan株式会社へ入社し、契約データベース「Contract One」の開発をしています。5人チームのリーダーとして、新規機能開発に向き合っています。

Contract Oneでは長らくKotlin 1.7.21を使用してきましたが、社内ハッカソンでKotlin 2.1へのアップデートを行ったので、紹介します。

buildersbox.corp-sansan.com

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分以内にするぞという意気込みのある方も募集中です。カジュアル面談など詳しくは採用情報をご確認ください。

media.sansan-engineering.com

© Sansan, Inc.