안녕하세요
오늘은 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를 언제 사용하면 좋을까요?
- 스마트 캐스트 개선
- 불필요한 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' 카테고리의 다른 글
[Kotlin] runCatching과 Result<T> (0) | 2025.04.10 |
---|---|
[Kotlin] Builder Pattern 대체하기 (0) | 2025.01.13 |
[Kotlin] Generic (3) - 그 외 (2) | 2024.12.19 |
[Kotlin] Generic (2) - 변성 (0) | 2024.12.10 |
[Kotlin] Generic (1) - 제네릭? (0) | 2024.12.10 |