지연 초기화 (Lazy Initialization) | Kotlin Study

hoya·2021년 12월 8일
1

Kotlin Study

목록 보기
1/7
post-thumbnail

평소에 안드로이드 프로젝트를 진행하면서 종종 뜨는 오류가 있었다.

kotlin.UninitializedPropertyAccessException: lateinit property adapter has not been initialized

바로 지연 초기화를 제대로 사용하지 않았을 때 발생하는 오류인데, 이번 기회를 통해 제대로 지연초기화에 대해 알아보고자 한다.


😪 Lazy Initialization

기본적으로 lazy 의 사전적 의미는 게으른, 느긋한, 여유로운 을 의미한다. 한마디로 위의 뜻을 직역하면 게으른 초기화 라는 의미인데, 게으른 초기화는 프로그래밍에서 꽤 긍정적인 의미로 쓰인다.

왜 지연 초기화를 사용할까?

지연 초기화라는 이름만 보아도 알 수 있듯이, 초기화 작업을 극한으로 미루다가 사용자가 필요로 할 때 진행하는데, 이 방법을 사용함으로서 메모리 낭비를 줄일 수 있다는 장점이 있다. 그리고 이는 퍼포먼스의 향상으로 이어진다.

또한 프로그램을 만들다보면 (val age: Int) 와 같은 변수를 선언하였지만 객체의 정확한 값을 뒤에 가서야 알 수 있는 경우 null 로 초기화할 수도 없어 굉장히 난감해진다. 이럴 때 지연 초기화를 사용함으로써 문제를 극복할 수 있다.

코틀린에서의 지연 초기화는 lateinit 을 사용하는 방법과 lazy를 사용하는 방법이 있다. 이 두 키워드를 천천히 알아보도록 하자.


📌 Lateinit

  • var 로 선언된 프로퍼티만 사용 가능하다.
  • Non-null 타입만 사용 가능하다.
  • 프로퍼티에 대한 getter, setter 를 사용할 수 없다.
  • 클래스 생성자에서 사용이 불가능하다.
  • 초기화 전에는 변수 접근이 불가능하다.
  • 원시 타입 (primitive type) 은 사용이 불가능하다.
  • 지역 변수에서 사용이 불가능하다.

생각보다 조건이 많지만, 막상 실제 코딩을 하다보면 상당히 많이 쓰이는 키워드 중 하나이다. 실제로 코드를 보며 어떤 때에 사용하는지 알아보도록 하자.

class Hoya {
    lateinit var nickname : String // 지연 초기화 선언
    // var nickname : String // 프로퍼티 자료형은 초기화를 해야하므로 오류 
    
    fun test() {
    	if(::nickname.isInitialized) { // 초기화 여부 판단
        	println("초기화 되었음.")
        }
    }
}

fun main() {
    val hoya = Hoya()
    hoya.test() // 초기화 되지 않은 시점
    // println("nickname = ${hoya.nickname}") // 이 시점에서 사용하면 오류 발생
    hoya.nickname = "hoyaho" // 이 시점에서 초기화
    hoya.test() // 초기화가 됐으므로 초기화 되었다고 알림
    println("nickname = ${hoya.nickname}")
}

주석을 읽으면 자연스럽게 이해가 될 것이다. 주의할 점으로, 초기화가 되지 않았는데 변수에 접근하면 오류가 발생하므로 주의해야 한다.


📌 Lazy

  • val 에서만 사용이 가능하다.
  • 이에 따라 값을 다시 변경할 수 없다.
  • 호출 시점에 by lazy { ... } 에 정의해둔 블록 부분의 초기화를 진행한다.
  • 클래스 생성자에서 사용이 불가능하다.
  • 원시 타입 (primitive type) 도 사용이 가능하다.
  • 지역 변수에서도 사용이 가능하다.
class HelloLazy {
    init {
    	println("init block") // 최초 초기화 선언을 알림
    }
    
    val subject : String by lazy { "Lazy Test" }
    
    fun flow() {
    	println("초기화 되지 않았음") // 아직 초기화 되지 않음
    	println("subject one : $subject") // 최초 초기화 시점
      	println("subject two : $subject") // 초기화된 값 사용 (불변)
    }
}

fun main() {
    val test = HelloLazy()
    test.flow()
}

최초 접근 시점에서 초기화하고 사용하는 것이 lazy 키워드의 핵심이라고 볼 수 있다. 또, by lazy에는 동기화와 관련해 여러 모드가 제공된다.

  • SYNCHRONIZED - 락을 사용해 단일 스레드만 사용하는 것을 보장한다. (DEFAULT)
  • PUBLICATION - 여러 군데에서 호출될 수 있으나, 처음 초기화된 반환 값만을 사용한다.
  • NONE - 락을 사용하지 않기 때문에 빠르나, 다중 스레드가 접근하여 값의 일관성을 보장하기 힘들다.

🖐 단일 스레드만 사용하는 것을 보장한다는게 어떤 의미인데?

만약, 한 변수에 여러 스레드가 접근한다면 어떻게 될지 생각해보면 간단하다. Thread1 이 하나의 변수를 건드리고 있는데, 갑자기 Thread2가 그 변수를 건드린다면? 값의 일관성을 보장할 수 없을 것이다. 즉, Thread-safe 를 보장하는 것이다.

    val lazyValue by lazy(LazyThreadSafetyMode.NONE) {
        // Initialize code block
    }

동기화가 필요하지 않은 환경에서 코드를 실행한다면, NONE 모드를 사용하여 성능을 높일 수 있을 것이다. 상황에 맞게 적절한 모드를 사용하도록 하자.

🥸 개인적으로 publication에 대해 조금 헷갈렸는데, 쉽게 이야기해 여러 스레드가 변수에 접근하는 것은 가능하지만 최초 접근한 스레드가 값을 초기화했다면 그 값만을 사용할 수 있다고 보면 된다.


📌 Lateinit vs Lazy


참고 및 출처

네이버 부스트코스 - 코틀린 프로그래밍

profile
즐겁게 하자 🤭

0개의 댓글