지연 초기화 (Lazy Initialization) | Kotlin Study
평소에 안드로이드 프로젝트를 진행하면서 종종 뜨는 오류가 있었다.
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
📌 in Android
안드로이드에서 UI 컴포넌트를 선언할 때 지연 초기화를 많이 사용하는데, by lazy
키워드가 효과적인지, lateinit
키워드가 효과적인지 알아보도록 하자.
private val tv : TextView by lazy { findViewByid . . . }
lateinit var tv2 : TextView
override fun onCreate(...) {
tv.text = "hoya" // 이 시점에서 초기화
tv2 = findViewById(R.id.tv2) as TextView // 개발자가 다시 초기화 후 사용해야 함
tv2.text = "hoya" // 초기화 작업없이 실행할 경우 오류 발생
}
위에서 보다시피 by lazy
로 지연 초기화를 진행할 경우 개발자가 초기화와 관련된 오류를 범할 일이 없어지는 반면에 lateinit
의 경우 개발자가 직접 초기화 작업을 수행해야 하는데, 만약 빠뜨리는 실수를 범하게 되면 오류가 발생하게 되므로 안드로이드에서 UI 컴포넌트를 선언할 때는 by lazy
키워드가 좀 더 안전할 것이다.
참고 및 출처
Author And Source
이 문제에 관하여(지연 초기화 (Lazy Initialization) | Kotlin Study), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@hoyaho/지연-초기화-Lazy-Initialization-|-Kotlin-Study저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)