본문 바로가기

Kotlin

[Kotlin] Contract

powered by pixabay

 

안녕하세요

오늘은 Kotlin의 Contract에 대해 알아보도록 하겠습니다


📌 서론

'Effective Kotlin Item 43. API의 필수적이지 않은 부분을 확장 함수로 추출하라' 에서 아래 코드를 보게 되었습니다

@OptIn(ExperimentalContracts::class)
inline fun CharSequence?.isNullOrBlank(): Boolean {
    contract {
        returns(false) implies (this@isNullOrBlank != null)
    }

    return this == null || this.isBlank()
}

 

해당 문법이 무엇인지 궁금해 찾아보게 되었습니다


📌 Contract?

Kotlin의 Contract는 컴파일러에게 함수의 실행 결과에 대한 추가적인 정보를 제공합니다

→ 스마트 캐스트 및 최적화를 도와주는 기능입니다

 

✅ When?

Contract를 언제 사용하면 좋을까요?

  1. 스마트 캐스트 개선
  2. 불필요한 null 체크 방지

Case1) 스마트 캐스트

contract 없이 isNotNull 메서드를 사용 시 → 스마트 캐스트가 안됩니다

fun isNotNull(value: Any?): Boolean {
    return value != null
}

fun main() {
    val str: String? = "Hello"

    if (isNotNull(str)) {
        // ❌ str이 여전히 Any? 타입 → 스마트 캐스트 안됨
        println(str.length) // 컴파일 오류
    }
}

 

contract를 사용하면 스마트 캐스트가 가능합니다

import kotlin.contracts.*

@OptIn(ExperimentalContracts::class)
fun isNotNull(value: Any?): Boolean {
    contract {
        returns(true) implies (value != null)
    }
    return value != null
}

fun main() {
    val str: String? = "Hello"

    if (isNotNull(str)) {
        // ✅ str이 String으로 스마트 캐스트됨!
        println(str.length) // 정상 작동
    }
}
  • returns(true) implies (value != null)
    • → isNotNull()이 true를 반환하면 value는 null이 아님을 보장

Case2) requireNotNull method

requireNotNull() 사용 시 → 스마트 캐스트가 가능합니다

fun process(value: String?) {
    requireNotNull(value) { "value is null" }
    println(value.length) // 가능한 코드
}

 

이는 requireNotNull 메서드 내부에 contract가 있어서 가능한 것입니다

public inline fun <T : Any> requireNotNull(value: T?, lazyMessage: () -> Any): T {
    contract {
        returns() implies (value != null)
    }

    if (value == null) {
        val message = lazyMessage()
        throw IllegalArgumentException(message.toString())
    } else {
        return value
    }
}

📌 코드 분석

제일 처음 봤던 코드를 다시 살펴봅시다

@OptIn(ExperimentalContracts::class)
inline fun CharSequence?.isNullOrBlank(): Boolean {
    contract {
        returns(false) implies (this@isNullOrBlank != null)
    }

    return this == null || this.isBlank()
}
  • 이 함수가 false를 반환하면 this는 null이 아니다
    • 이 정보를 컴파일러에게 알려줍니다
  • 즉, isNullOrBlack()가 false를 반환하면, 컴파일러는 this가 null이 아니라고 확신할 수 있습니다

 

아래 코드도 분석해보겠습니다

public inline fun <T : Any> requireNotNull(value: T?, lazyMessage: () -> Any): T {
    contract {
        returns() implies (value != null)
    }

    if (value == null) {
        val message = lazyMessage()
        throw IllegalArgumentException(message.toString())
    } else {
        return value
    }
}
  • 이 함수가 정상적으로 반환된다면, value는 null이 아니다
  • 즉, 예외를 던지지 않고 실행이 완료된다면 value가 null이 아님을 보장합니다

📌 성능 분석

성능 상의 차이는 없을지 궁금해져서 benchmark를 사용해 비교해봤습니다

 

✅ 코드

성능 비교 대상이 될 코드 두개를 준비해 줍니다

  • contract 쓰지 않는 함수 - withOutContract()
  • contract 쓰는 함수 - withContract()
package com.example.benchmark

import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

class ContractBenchmark {
    fun withOutContract() {
        val str: String? = "Hello"

        if (withOutIsNotNull(str)) {
            println(str?.length)
        }
    }

    fun withContract() {
        val str: String? = "Hello"

        if (withIsNotNull(str)) {
            println(str.length)
        }
    }

    private fun withOutIsNotNull(value: Any?): Boolean {
        return value != null
    }

    @OptIn(ExperimentalContracts::class)
    private fun withIsNotNull(value: Any?): Boolean {
        contract {
            returns(true) implies (value != null)
        }
        return value != null
    }
}

 

벤치마크로 성능 분석을 할 수 있게 만들어 줍니다

package com.example.benchmark

import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class ContractBenchmarkTest {

    @get:Rule
    val benchmarkRule = BenchmarkRule()

    private val contractBenchmark = ContractBenchmark()

    @Test
    fun withOutContract() {
        benchmarkRule.measureRepeated {
            contractBenchmark.withOutContract()
        }
    }

    @Test
    fun withContract() {
        benchmarkRule.measureRepeated {
            contractBenchmark.withContract()
        }
    }
}

 

✅ 결과

결과는 아래와 같이 나왔습니다

contract를 사용한 것이 조금 더 느리게 나왔지만, 유의미한 차이는 아닌 것 같습니다

 

✅ Bytecode

decompiled java byte code를 확인해보니 아래와 같이 차이가 납니다

public final void withOutContract(
   String str = "Hello";
   if (this.withOutIsNotNull(str))
      Integer var2 = str.length();
      System.out.println(var2);
   }
}

public final void withContract() {
   String str = "Hello";
   if (this.withIsNotNull(str)) {
      int var2 = str.length();
      System.out.println(var2);
   }
}
  • Integer → Nullable 한 정수
  • int → Non-Null 정수

이를 제외하고는 차이가 없어서, 결과도 미미한 차이가 났던 것 같습니다


📌 정리

  • contract는 컴파일러가 코드 흐름을 더 정확히 이해하도록 도와줍니다
  • nullable 검사, 스마트 캐스트, 불필요한 null 체크 제거 등에 활용 가능합니다
  • 코드 가독성과 안정성을 높일 수 있습니다

'Kotlin' 카테고리의 다른 글