모종닷컴

Kotlin Contract 본문

Programming

Kotlin Contract

모종 2023. 7. 28. 20:45
반응형

요즘 Coroutine을 학습 중에 있습니다. 좀 Deep dive를 하자는 의미에서 디버깅을 시작하는데 시작하자마자 턱 막혀버렸습니다. 오늘의 주인공 contract입니다. Coroutine에서 만든 API인 줄 처음에 착각했는데 막상 보니 코틀린 표준 라이브러리에 들어가 있는 친구였습니다.

 

Kotlin Contract

  • Kotlin 1.3 이후부터 지원하기 시작한 실험적인 API 
  • 코틀린 컴파일러에게 함수의 동작을 명시적으로 설명해주는 것

Kotlin Compiler

  • 코틀린으로 작성된 소스 코드를 JVM이 이해할 수 있는 바이트코드(.class 파일)로 변환하는 일을 담당
  • 저희가 작성한 코드를 정적 분석을 통해 바이트코드로 전환할 때 유용한 기능들이 있는데 그중 하나가 스마트 캐스트입니다

 

스마트 캐스트

스마트 캐스트 예제를 보겠습니다.

sealed class User {
    fun isAuthenticated(): Boolean {
        return this is Authenticated
    }

    class Anonymous : User() {
        fun promptToSignIn() = println("Please sign in.")
    }

    class Authenticated(val userName: String) : User() {
        fun greet() = println("Welcome, $userName!")
    }
}

fun onScreenLoaded(user: User) {
    when(user) {
        is User.Authenticated -> user.greet()
        is User.Anonymous -> user.promptToSignIn()
    }
}

internal class ContractTest {
    @Test
    fun test() {
        onScreenLoaded(User.Anonymous())
    }
}

onScreenLoaded 함수를 보시면 파라미터가 user입니다. 그리고 when 절에서 이 유저의 타입을 체크를 하고 나서 block 안을 보면 이미 타입이 캐스트가 된 것처럼 각 타입에 맞는 메서드를 사용할 수 있게 됩니다.

Intellij IDEA를 사용하시는 분은 이 코틀린 코드를 자바로 변환된 모습을 볼 수 있는데 Shift Key 두 번 연속으로 누른 후 Show Kotlin Bytecode를 선택해줍니다. 그럼 우측에 Kotlin Bytecode 탭이 생기는데 Decompile을 눌러주면 자바로 변환된 파일을 볼 수 있습니다.

변환된 자바 코드의 onScreenLoaded 부분을 보면 타입 체크 이후 강제 형변환을 해주고 있는데, 이 형변환은 코틀린 컴파일러가 만들어준겁니다.

바보 코틀린 컴파일러

코틀린 컴파일러는 한계점이 있습니다. onScreenLoaded 함수를 아래와 같이 수정을 해보겠습니다. 

fun onScreenLoaded(user: User) {
    when(user.isAuthenticated()) {
        true -> user.greet()
        false -> user.promptToSignIn()
    }
}

수정하고 나면 컴파일러의 스마트 캐스트가 동작하지 않습니다. 공식 문서에 따르면 코틀린 컴파일러는 분리된 function에서 체크되었던 사항은 즉시 사라진다고 합니다.

대표적인 스마트 캐스트가 안 되는 경우

좀 더 흔한 예제를 들자면 isNotNull 같은 확장 함수가 있겠습니다.

fun length(s: String?) {
    if (s != null) {
        println("length = ${s.length}") // 스마트 캐스트 적용
    }
}

fun lengthV2(s: String?) {
    if (isNotNull(s)) {
        println("length = ${s!!.length}") // 스마트 캐스트 미적용
    }
}

fun isNotNull(s: String?): Boolean {
    return s != null
}

 

Contract로 컴파일러에게 친절한 설명을 해주자

그럼 contract를 적용한 모습을 보도록 하겠습니다. 아까 고친 코드를 보면 isAuthenticated 함수를 호출한 이후 체크된 사항들이 즉시 사라진다고 했죠? isAuthenticated를 아래와 같이 수정해 보겠습니다.

@OptIn(ExperimentalContracts::class)
fun isAuthenticated(): Boolean {
    contract {
        returns(true) implies (this@User is Authenticated)
        returns(false) implies (this@User is Anonymous)
    }
    return this is Authenticated
}

해석을 하자면 이 함수를 호출했을 때 리턴 값이 true인 경우 이 유저는 Authenticated 타입, false면 이 유저는 Anonymous 타입이라는 것을 코틀린 컴파일러에게 알려주고 있는 겁니다.

이제 아까 오류가 났던 onScreenLoaded 함수 쪽을 다시 가보면 이제는 오류가 사라져 있음을 볼 수 있습니다.

isNotNull 확장 함수도 고쳐보자

아까 추가적인 예시로 isNotNull 예제를 봤었죠? 이 함수도 contract를 사용해 보겠습니다. 

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

그럼 아래와 같이 더 이상 오류가 나지 않고 스마트 캐스트가 잘 되고 있음을 볼 수 있습니다.

반응형