메이쁘

[Kotlin] contract 에 대한 정확한 이해 - contract는 컴파일러에게 보여주는 용도!(+ callsInPlace 활용) 본문

Language/Kotlin

[Kotlin] contract 에 대한 정확한 이해 - contract는 컴파일러에게 보여주는 용도!(+ callsInPlace 활용)

메이쁘 2022. 3. 30. 14:43

안녕하세요?

 

개인적으로 코틀린 문법 공부하다가 contract에 대해 알게 되었고,

샘플 코드를 작성해가면서 이해하고 있었습니다.

 

그러던 중, 샘플 코드가 제대로 동작하지 않아서 원인을 찾던 중 좀 다르게 이해하고 있었습니다..

 

그래서, 혹시나 까먹지 않고 나중에 다시 사용할 때 기억하기 위해 간단하게라도 포스팅하게 되었습니다!

(다른 블로그 게시글을 참고하면서 공부했습니다.)

 

 


0. Contract 란?

우선, contract에 대한 정의와 사용 목적에 대해 간단히 작성하겠습니다.

 

------------------------------------------------------------------------------------------------------------------------------------------

Contract 

 *  함수가 컴파일러가 이해하는 방식으로 동작을 명시적으로 설명할 수 있도록 한다.
 *  ex) null check하기 위한 함수에 @ExperimentalContracts와 contract{} 를 통해 null check 하고,
 *  이 함수를 호출한 다음에는 parameter로 들어온 값을 null 이 아니다. 라고 컴파일러를 이해시킴.
 *  그래서 이후 코드에는 ?나 !!를 사용하지 않아도 동작한다.

------------------------------------------------------------------------------------------------------------------------------------------

 

 

즉, contract가 포함된 함수가 있을 때, contract 코드를 컴파일러에게 미리 이해시켜주기 위해 사용한다고 생각하시면 됩니다. 이를 명시적으로 표현하는 것이구요.

 

그래서 보통 contract가 포함된 함수를 호출한 다음 로직부터는

contract에 정의한 조건을 계속 명시하지 않아도 된다는 장점이 있습니다.

 

대표적으로는 null check - nullable(? 또는 !! 명시 등..) 를 따로 하지 않아도 컴파일 오류가 나지 않는다는 점이죠.

코딩에 있어서도 효율성이 높아질 것 같습니다.

(아직 실제로 활용해보지 않았지만..)

 

이를 사용하기 위해선 @ExperimentalContracts 어노테이션을 붙여줘야 하는데요! contract가 포함된 함수 및 이를 사용하는 함수에 표기하면 됩니다.

 

 


1. contract 예제

 

@ExperimentalContracts
        fun isNotNullAndReturnBoolean(value: Any?): Boolean {
            contract {
                returns(true) implies (value != null)  // value가 null이 아니면, true를 리턴하면서 컴파일러에게 value가 null이 아님을 이해시켜줌.
            }

            return value != null 
        }

위 코드가 마무리한 예제 코드 입니다! 실제 이런 방식으로 contract를 활용합니다.

 

그렇지만..

처음 작성한 예제 코드는 이게 아닙니다.

 

이전에는 return value != null 이 아니라, return false 로 작성했습니다.

제가 이전까지 이해하기로는,

contract {

  returns() implies ()

}

에서 implies () 에 있는 () 안의 조건이 만족하면 returns() 에서 () 안의 값으로 리턴해서 함수가 종료되는 줄 알았습니다.

 

 

그래서,

아래와 같이 작성했을 때, parameter로 null이 아닌 값이 들어오는 경우, contract {} 안에 returns(true) 이기 때문에 이 함수는 true를 리턴해주고 종료되는 것인줄 알았습니다..

 

하지만, 아니었습니다. (바보같이 이걸 헷갈리냐 할 수 있겠지만..)

 

 

결론은 contract { } 의 returns()는 실제 코드 동작에 반영되는 것이 아니라, 컴파일러에게 결과를 전달하기 위함이었습니다.

 

 

즉,

contract { } 의 리턴 값이 true면 "컴파일러야. 이 조건은 참이야. 기억해!" 이고,

false면 "컴파일러야. 이 조건은 거짓이야. 신경쓰지말고 할일 계속 해!" 입니다.

 

 

그렇기 때문에, implies () 의 조건이 어떻든 간에 contract { } 는 그대로 벗어나고, 다음 코드를 이어서 진행합니다.

그래서 위 코드는 value의 값이 null이든, null이 아니든 무조건 false를 리턴해주게 되었습니다.

 

 

위 코드 작성까지는 테스트가 잘 동작하길래 모르고 지나쳤지만..

다음 코드에서 확인할 수 있었습니다.

 

 


2. 발견(+디버깅)

contract는 True/False의 boolean과, null 을 리턴할 수 있습니다. 

returns()의 ()가 비어있으면 null을 리턴합니다.

 

이 내용에 대한 예제코드를 만들고, 테스트를 해봤습니다..

위 코드상으로는 value가 null이 아니기 때문에 Exception을 throw하지 않을 거라고 생각했는데, 

계속 에러를 뱉어내는 것입니다..!!

 

이해가 잘 안되서 디버깅을 해봤습니다.

 

value에는 null이 아닌 제대로된 값이 들어왔는데, Exception으로 가는 것이었습니다.

 

 

그래서 왜그럴까 하고, 

contract를 활용한 기본 코드 중 자주 사용하는 String.isNullOrBlank() 함수를 확인해봤습니다.

 

isNullOrBlank()

여기서도 contract를 벗어난 다음 코드에서도 조건에 따른 return 처리를 작성해놨습니다.

 

 

이를 통해서,

 

contract { } 의 returns()는 실제 코드 동작에 반영되는 것이 아니라, 컴파일러에게 결과를 전달하기 위함이고, 결과 여부에 관계없이 이어서 동작한다.

 

를 깨닫게 되었습니다.

 

 


3. contract 활용

원래라면, 

val value:String 으로, not-null로 선언한 변수에 val value:String? 인 nullable 변수를 대입할 수 없습니다.

 

하지만, 위 코드에서

contract { } 에서 이미 nullableValue 변수 값이 null이 아님을 컴파일러가 이해했기 때문에, if절 내부에서 대입할 수 있습니다.

이 뿐 아니라, nullable 기호도 명시하지 않아도 됩니다.

왜냐하면? -> nullableValue 변수 값이 null이 아님을 컴파일러가 이해했기 때문에!

 

이 코드도 마찬가지로, exception을 발생하지 않고 함수가 그대로 종료되었기 때문에

이후 코드에서 nullable을 명시할 필요가 없습니다!

 

 

 


4. callsInPlace 활용

위처럼, @ExperimentalContracts 어노테이션과 contract { } 를 함께 사용하는 것 중 하나로 callsInPlace() 가 있습니다.

 

callsInPlace는 람다 함수를 사용할 때, 그 함수의 호출 횟수를 명시적으로 컴파일러에게 이해시켜주기 위해 사용합니다.

이는 왜 쓰느냐??

 

 

만약, 위처럼 callsInPlace를 사용하지 않은 일반적인 람다 호출 함수일 경우에는

 

fun main() 에서 작성한 코드입니다.

val로 선언한 값이 정의되어있지 않기 때문에,

 

  - Captured ~ : invokeLambda 함수가 몇 번 호출되는지 명확히 모르기 때문에, val 변수 특성 상 여러 번 값이 변할 수 있어 오류 발생

  - Variable ~ : 마찬가지로, invokeLambda 함수가 몇 번 호출되는지 명확히 모르기 때문에, return 값이 다를 수 있어 오류 발생 

 

이 납니다.

 

즉, 이걸 개발한 개발자는 당연히 저거만 짜놨으니까, 한 번만 호출하는데 왜 이해못하지?

할 수 있지만, 컴파일러 입장에서는 확실하지 않다는 겁니다.

 

 

이걸 해결하기 위해 callsInPlace를 사용합니다.

1번 째 parameter는 호출할 람다 함수, 2번 째 parameter는 호출 횟수입니다.

import kotlin.contracts.InvocationKind

InvocationKind에는

 

  -  AT_LEAST_ONCE : 최소 1번 이상 실행(1, N)

  -  AT_MOST_ONCE : 최대 1번 실행(0, 1)

  -  EXACTLY_ONCE : 정확히 1번 실행(1)

속성이 있습니다.

 

상황에 알맞게 적용시키면 됩니다.

 

 

오류 발생 X

 

 

 

 

이상입니다.

 

틀린 부분 및 보충설명 필요한 게 있으면 댓글 부탁드립니다.

 

 

감사합니다!

 

 

참고

https://medium.com/harrythegreat/kotlin-contracts-%EB%AC%B8%EB%B2%95-%EC%89%BD%EA%B2%8C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EA%B8%B0-9ffdc399aa75

Comments