즉시 계산과 지연 계산

프로그래밍 언어에는 즉시 계산 언어지연 계산 언어 가 있다. Java나 Kotlin은 모든 내용이 즉시 평가되는 즉시 계산 언어이다.

val x: Int = 2 + 3
val y: Int = getVaule()

x는 즉시 계산되어 5로 평가된다. 두 번째 식 또한 y 참조가 선언되자마자 getValue() 함수가 호출되어 y에 값을 제공한다.


프로그래밍은 ‘실행되는 시점’에 평가가 이뤄질 프로그램 명령어를 조합하는 것으로 이루어진다. 때문에 기본적으로 프로그램이라는 것은 기본적으로 지연 계산 방식으로 이루어진다. 만약 프로그램이 완벽하게 즉시 계산만을 사용한다면 코드를 짜고 엔터를 누르는 것과 동시에 바로 실행될 것이다.

하지만 코틀린에서도 지연 계산이 필요한 때도 있다. 예를 들자면 아래와 같은 if-else 구문이 있다고 하자.

val result =
    if (isSuccess()) {
        getTrueValue()
    } else {
        getFalseValue()
    }


isSuccess() 는 항상 실행되지만, 이 함수의 결과에 따라서 getTrueValue() 와 getFalseValue() 중에서 하나만 호출된다.

  • 만약 이 코드가 완전한 즉시 계산을 사용하는 구조였다면 if-else 와 관계 없이 두 함수가 모두 처리되었을 것이다.
  • 반대의 경우였다면 getTrueValue()와 getFalseValue()가 둘 다 계산된 후에 isSuccess()에 따라 한 쪽 값을 반환하기 때문에 처리 시간이 더 오래 걸릴 것이다.


이러한 이론적인 예제 뿐 아니라 실질적으로 안드로이드 앱 개발을 하면서 지연 계산이 필요할 때가 있다. 내가 코딩을 하면서 가장 빈번하게 찾은 사례는 객체 변수의 선언과 초기화를 하는 일이었다. 개인적인 생각으로는, 자바에서 코틀린으로 안드로이드 추세가 변해갔을 때 언어 컨버팅을 하면서 자바식 코드 스타일이 남아 있어서 자주 보는 것 같기도 하다.


Kotlin 에서 변수 선언 이후에 초기화하기

Java에서는 변수 선언부터 먼저 하고 나중에 초기화를 해도 문제가 되지는 않는다.

String str;


Kotlin에서는 선언과 동시에 초기화하지 않으면 빌드할 수 없으며 다음과 같은 에러 메세지가 나타난다.

var str: String //ERROR

Property must be initialized or be abstract


그러면 이런 상황은 언제 발생할까?

값을 아직 계산할 수 없는 경우, 계산된 결과가 실제로 쓰이지 않을 가능성이 있는 경우 등이 그러한 예시이다.

  • 값을 아직 계산할 수 없는 경우란 Context 등의 문제로 인자를 생성할 때에는 초기화가 불가능하여 참조하는 시점에서 계산해야 한다
  • 값을 계산할 수 있다고 하더라도, 후자와 같은 상황에서는 불필요한 리소스 낭비가 발생할 것이다.

이것을 방지하기 위해 코틀린에서 lateinitlazy 를 사용한다.


lateinit

Late, Init. 단어 그대로 ‘나중에 초기화할게!’라고 말해주는 속성이다. var 앞에 단어를 붙여 주기만 하면 된다.

lateinit var currentTime: String //type은 명시해주어야 한다

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    currentTime = "현재시각 : ${Calendar.getInstance().time}"
}

lateinit은 선언 이후 나중에 초기화를 해야하기 때문에 Immutable 변수에는 사용할 수 없다. 즉 var 변수에서만 사용 가능하다.

또한 초기화를 하기 전엔 변수에 접근이 불가능하다. 다시 말하면 lateinit을 사용하는 인자는 호출하는 타이밍에 따라 null이 될 확률을 가지고 있다. null이 요구되는 상황이 아니라면 에러를 야기시킬 수 있다. var 이기 때문에 나타나는 상황들인데, 아래에서 보는 lazy는 반대로 val 형태로 사용 가능하다.


lazy

lazy로 인한 초기화는 호출 시점에 정의된다. 한 번 정의되면 변하지 않는 val 변수이다. 사용 방법은 val 변수 선언 뒤에 by lazy {...} 블럭을 붙여 주면 된다.

val currentTime: String by lazy {
        "현재시각 : ${Calendar.getInstance().time}"
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    //이 시점에서 초기화됨
    textView.text = currentTime
}

textView에서 currentTime의 값을 사용하지 않는다면 현재 시각을 계산하지 않을 것이고, 사용한다면 그 시점에 계산할 것이다.

lazy는 non-nullable이며 빈 값으로 선언을 해두는 형식이 아니기 때문에 lateinit에 비해 null에 대한 걱정이 적다. 또한, 기본적으로 Synchronized 하게 동작하기 때문에 스레드로부터 안정성도 있다.



References

  • 피에르 이브 쏘몽 저,오현석 번역, 『코틀린을 다루는 기술』, 길벗(2020)
  • https://medium.com/til-kotlin/how-kotlins-delegated-properties-and-lazy-initialization-work-552cbad8be60