ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 🦥 lateinit 과 lazy
    🔖 Kotlin 2024. 12. 29. 15:33

    💡 지연초기화

    초기화를 미뤘다 실제 사용하는 시점에 초기화하는 기법

    • 초기화 과정에서 자원을 많이 사용하거나 오버헤드가 발생하는 경우 지연초기화를 사용하는 것이 유리

    ❗ 코틀린에서는 두 가지의 다른 방식의 지연초기화를 제공

    • lateinit, lazy
    • 변수를 초기화하는 시점을 제어해 성능 최적화와 코드 가독성을 높인다.

     

    📌 lazy

    class HelloBot {
        var greeting: String? = null
        fun sayHello() = println(greeting)
    }
    fun getHello() = "안녕하세요"
    fun main() {
        val helloBot = HelloBot()
        helloBot.greeting = getHello()
        helloBot.sayHello()
    }
    

     

    ⚠️ 위 코드의 문제점?!

    • 지연할당을 위해서 var 키워드를 이용해 변수를 선언해주었다.
       
    값 변경 가능성 var로 선언된 변수는 가변적이기 때문에, 코드 어느 지점에서든 값이 변경될 수 있다.
      의도치 않게 변경되면 프로그램 동작이 비정상적으로 변할 수 있고 디버깅을 어렵게 한다.
    초기 상태와 사용 지점의 불일치 초기화되지 않은 상태(null 인 상태)에서 사용될 위험이 있다.
      null을 허용하기 때문에 개발자가 사용 시점에 null 체크를 누락시, NPE 발생할 수 있다.

     

     

    📌 val과 lazy를 활용해 해결

    • 코틀린에서는 이와 같은 상황에서 lazy 라는 기능을 사용해 불변성을 유지하면서 지연 초기화를 할 수 있다.
    • 첫 호출시점에 초기화를 해주고 이후에는 값이 불변함을 보장
    class HelloBot {
        val greeting: String by lazy { getHello() }
        fun sayHello() = println(greeting)
    }
    fun getHello() = "안녕하세요"
    fun main() {
        val helloBot = HelloBot()
        helloBot.sayHello()
    }
    

     

     

    👋 결과

    • 가변 변수를 사용하지 않고, 불변 변수(val)과 lazy를 활용하면 변수가 한 번만 초기화되고 이후 변경되지 않도록 보장할 수 있다.
    • 초기화는 실제 필요한 시점까지 지연되어 메모리 사용도 최적화된다.

     

    👍 by lazy는 멀티스레드 환경에서도 안전하게 동작하도록 설계가 되어 있다.

    fun main() {
        val helloBot = HelloBot()
        for (i in 1 until 5) {
            Thread {
                helloBot.sayHello()
            }.start()
        }
    }
    
    • 이럼에도 초기화 로직은 단 한번만 실행이 된다.
    • 기본적으로 멀티스레드에 안전하게 설계가 되어 있다는 것을 의미

     

    ❗ by lazy 같은 경우 이렇게 멀티스레드 환경에서 안전하게 사용할 수 있지만, 해당 락에 대한 처리를 끄고서도 동작시킬 수 있다.

    • by lazy의 default = LazyThreadSafetyMode.SYNCHRONIZED

    LazyThreadSafetyMode.NONE 를 사용한 코드

    class HelloBot {
        val greeting: String by lazy(LazyThreadSafetyMode.NONE) {
            println("초기화 로직 수행")
            getHello()
        }
        fun sayHello() = println(greeting)
    }
    fun getHello() = "안녕하세요"
    fun main() {
        val helloBot = HelloBot()
        for (i in 1 until 5) {
            Thread {
                helloBot.sayHello()
            }.start()
        }
    }
    • 초기화 로직이 한 번만 수행되지 않는다.
    • 실행할 때마다 일관성 없게 동작

     

    ⚠️ 멀티 스레드 환경이 아니라면 동기화 작업이 오히려 오버헤드일 수 있다.

    만약 멀티 스레드 환경인 경우에도 동기화 작업이 필요없다면? ⇒ PUBLICATION 옵션을 사용할 수 있다.

     

    📌 lateinit

    특정 상황, 불변 객체에 대한 초기화를 제공하지 않아 가변 변수를 써야하는 상황이 있을 수 있다.

    이런 경우에서 지연 초기화가 필요하다면 lateinit 기능을 사용

     

     

    ex) JUnit 테스트 환경

    @Autowired
    lateinit var service: TestService
    
    lateinit var subject: TestTarget
    
    @SetUp
    fun setup() {
        service = TestTarget()
    }
    
    • setup 메서드 안에서 객체를 초기화하는 경우 lazy를 사용할 수 없다.

     

    class LateInit {
        lateinit var text: String // 무조건 var (val 타입은 컴파일에러)
    }
    

     

     

    📖 특징

    1. lateinit var 로 선언한다. (var, 가변 변수만 가능)
    2. 변수를 선언할 때 초기화하지 않고 적절한 시점에 초기화
    3. lateinit은 nullable이 아닌 타입에 사용할 수 있다.
      • nullable 하게 선언시 컴파일 에러 발생 (ex. String?)
      • 항상 Non-Null 이라 생각하면 된다.
    4. ⚠️ 사용 전에 반드시 초기화해야 하며 초기화되지 않은 상태에서 접근 시 UninitializedPropertyAccessException 발생
      • 컴파일러가 초기화 여부를 확인하지 못하기 때문에 관리를 해주지 않으면 런타임 에러 발생
    5. Primitive Type(Int, Float, Double, Long 등)에 사용할 수 없다.

     

    📍 isInitialized

    특정 프레임워크나 라이브러리에서 DI나 외부에서 초기화를 해주는 경우를 염두한 후 만든 기능이기 때문에 초기화 전에 사용하더라도 컴파일 에러가 발생하지 않는다.

    • late init으로 된 변수에 대한 초기화 여부를 확인하기 위해서는 isInitialized 라는 프로퍼티를 사용해주어야 한다.
    class LateInit {
        lateinit var text: String 
    
        fun printText() {
            if(this::text.isInitialized) {
                println("초기화됨")
            } else {
                text = "안녕하세요"
            }
            println(text)
        }
    }
    

     

    🤔 왜 non-null 프로퍼티로만 사용가능할까?

     

    1️⃣  초기화 지연의 목적

    • 객체가 반드시 초기화될 때까지 null 상태를 허용하지 않고 접근을 방지
    • null을 허용할 시 변수를 사용하기 전에 매번 명시적으로 null 체크가 필요
    • lateinit은 반드시 초기화가 이루어져야 하는 변수에 대해서만 사용하도록 설계되었기 때문에 null을 할당 할 수 없도록 제한

     

    2️⃣ 변수의 초기화 보장

    • lateinit 변수는 초기화되지 않은 상태로 접근 시 예외가 발생한다 (UninitializedPropertyAccessException)
    • 이때, 만약 null을 허용하게 된다면 초기화되지 않은 변수에 접근 시 null이 반환되거나 예외가 발생하지 않아 초기화가 되지 않은 상태를 체크하지 않은채 사용될 수 있다.
    • 따라서, 초기화되지 않은 상태에서 안전하지 않은 동작을 방지하기 위해 null 할당을 허용하지 않는다.

    이렇게 lateinit은 null을 허용하지 않음으로써, null 체크나 초기화 여부를 강제하지 않고 프로퍼티가 꼭 초기화되도록 보장하는 역할을 한다.

     

    🤔 왜 Primitive Type에 사용할 수 없을까?

    private lateinit var x: Int
    

     

    lateinit의 내부 구현에는 null 과 비교하여 초기화여부를 판단한다.

    하지만 primitive type에는 null을 넣을 수 없다.

     

    private lateinit var x: Int? // 그럼 nullable한 타입으로 변경을 하면?
    

     

    lateinit 변수가 null 값을 가질 수 있게 된다면 아까 null을 허용했을 때의 문제들이 발생하기 때문에 nullable type을 사용할 수 없다.

     

    📖 참고자료

    [Kotlin] Kotlin Lazy Initialization

    Kotlin by lazy 분석해보기

    LazyThreadSafetyMode

    Why Kotlin lateinit Can’t Be Used With Primitive Types | Baeldung on Kotlin

    '🔖 Kotlin' 카테고리의 다른 글

    😎 Infix Function  (0) 2024.12.30
    널 안정성  (0) 2024.12.29
    Kotlin Coroutines  (0) 2024.12.28
    Unit / Nothing  (0) 2024.11.25
    Gradle Kotlin DSL  (2) 2024.11.21

    댓글

Designed by Tistory.