[Kotlin] 지연초기화(lateinit, lazy)

Hanbin·2021년 8월 24일
0

Kotlin

목록 보기
2/3
post-thumbnail

💡 지연초기화란?

일반적으로 propertienon-null type으로 선언하기 위해선 생성자에서 초기화가 필요하다. 하지만 생성자에서 초기할 수 없거나, null type을 피하고 싶은 경우, 사용 시점에 초기화하고 싶은 경우를 위해 지연초기화(Late initialization, Lazy initialization)를 제공한다.

1. Late initialization

  • var 앞에 lateinit 키워드를 사용하여 선언한다.
    • lateinit var string: String
  • 클래스 내부나 최상위 레벨, 지역변수로 사용할 수 있다.
  • non-null type만 가능하고 primitive type은 허용되지 않는다.
  • 초기화하지 않고 사용할 경우 Exception이 발생한다.
    • lateinit property string has not been initialized

lateinit var 초기화 여부 확인

초기화되어 있는지 확인하려면 property 앞에 :: 키워드를 사용해 .isInitialized를 호출한다.

class LateInitTest {
    latinit var string: String
    
    @Test
    fun test() {
        // println(string) // Exception
        if (!::string.isInitialized) {
            string = "init"
        }
        println(string)
    }
}

2. Lazy initialization

  • val 에서만 사용 가능하고, by lazy 사용하여 선언한다.
    • val string: String by lazy { "init" }
  • 첫 번째 호출은 by lazy에 전달된 람다를 실행하고 그 결과값을 저장한다.
  • 기본적으로 값은 하나의 스레드에서만 계산되고 모든 스레드는 동일한 값을 보게 된다.
class LazyTest {

    val lazyValue: String by lazy {
        println("computed!")
        "Hello"
    }

    @Test
    fun test() {
        println(lazyValue)
        println(lazyValue)
    }
}

computed!
Hello
Hello

위의 출력 결과를 살펴보면 해당 프로퍼티 최초 접근 시 람다식이 호출되며 그 이후에는 해당 람다식의 결과값을 return 한다.

lazy 내부코드

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)
}

내부 코드를 살펴보면 최초 _value는 UNINITIALIZED_VALUE(internal object UNINITIALIZED_VALUE)로 초기화된다.

get()이 호출될 때 _value가 UNINITIALIZED_VALUE가 아니면 _value를 return 한다.

그 이후에는 synchronized로 thread safety 하게 동작하며 다시 한번 _value가 UNINITIALIZED_VALUE 인지 체크하며 UNINITIALIZED_VALUE인 경우 람다식을 호출하며 그 결과값을 저장한 뒤 return 한다.

🙏 마치며

lateinit은 생성자에선 초기화할 수 없지만 non-null하게 사용하고 싶은 경우, 특정 시점에 초기화하며 value를 지속적으로 변경해야 할 경우 사용한다.

lazy는 특정 시점에 초기화하고 싶은 경우, 최초 접근 시 value를 초기화하며 이후 value의 변경이 필요하지 않은 경우 사용한다.

📜 참고

0개의 댓글