관리 메뉴

나만을 위한 블로그

[Android] 지연 초기화, lateinit vs Lazy 본문

Android

[Android] 지연 초기화, lateinit vs Lazy

참깨빵위에참깨빵 2023. 1. 28. 00:54
728x90
반응형

24.07.15) lateinit var, by lazy의 커스텀 게터세터 불가능 확인 사례 추가

 

두 키워드는 모두 코틀린에서 지연 초기화 시 사용하는 키워드다. 두 키워드를 확인하기 전에 먼저 공통 개념인 지연 초기화부터 확인한다.

위키백과에서 설명하는 지연 초기화는 아래와 같다.

 

https://en.wikipedia.org/wiki/Lazy_initialization

 

Lazy initialization - Wikipedia

From Wikipedia, the free encyclopedia In computer programming, lazy initialization is the tactic of delaying the creation of an object, the calculation of a value, or some other expensive process until the first time it is needed. It is a kind of lazy eval

en.wikipedia.org

컴퓨터 프로그래밍에서 지연 초기화는 처음 필요할 때까지 객체 생성, 값 계산 또는 기타 비용이 많이 드는 프로세스를 지연시키는 전술(tactic)이다. 객체 또는 기타 리소스의 인스턴스화를 특별히 참조하는 일종의 게으른 평가(evaluation)다. 이는 일반적으로 접근자 메서드(또는 속성 getter)를 보강해 캐시 역할을 하는 전용 멤버가 이미 초기화됐는지 확인해 수행된다. 있다면 바로 반환된다. 그렇지 않으면 새 인스턴스가 생성돼 멤버 변수에 배치되고 최초 사용을 위해 적시에 호출자에게 반환된다. 객체에 거의 쓰이지 않는 속성이 있는 경우 시작 속도가 향상될 수 있다. 평균 프로그램 성능은 메모리(조건 변수의 경우) 및 실행 주기 측면에서 약간 더 나쁠 수 있지만 객체 인스턴스화의 영향은 시작 단계에 집중되기보다는 시간에 따라 분산(상각)된다. 따라서 평균 응답 시간을 크게 향상시킬 수 있다. 멀티쓰레드 코드에서 지연 초기화 객체 / 상태에 대한 접근은 경합 상태를 방지하기 위해 동기화돼야 한다

 

지연 초기화라는 말은 코틀린에만 존재하는 것은 아니고 C 계열 언어와 파이썬, 자바, 자바스크립트, PHP 등에도 사용되는 개념이다. 위 사이트에 들어가면 각 언어별 예시를 볼 수 있다.

위키백과 설명의 핵심은 지연 초기화란 객체 생성, 값 계산 등을 바로 수행하지 않고 최대한 미루다가 최초로 필요해진 시점에 수행하는 방법이라는 것이다.

코틀린에서의 지연 초기화도 이것과 크게 다르진 않겠지만 다른 곳에선 어떻게 설명하는지 확인해 본다.

 

https://medium.com/@joongwon/kotlin-kotlin-lazy-initialization-901079296e43

 

[Kotlin] Kotlin Lazy Initialization

오늘은 Lazy Initialization에 대해서 알아볼 것이다. Initialization는 초기화를 뜻하니 익숙하겠지만 Lazy에 대해선 익숙하지 않은 분들도 있을 것이다. Lazy의 사전적 의미는 다음과 같다.

medium.com

프로그래밍 언어 세계에서의 Lazy는 긍정적인 것에 가깝다. 프로그램 퍼포먼스를 높이기 위해 처음부터 모든 것들을 초기화하는 게 아닌 필요한 순간까지 최대한 초기화를 지연시킨다는 의미로 사용한다. 지연 초기화를 잘 썼다고 가정했을 때 소프트웨어의 성능 측면에서 이점이 있다. 클래스가 초기화될 때 모든 걸 동시에 초기화하는 코드와 필요한 순간까지 초기화를 최대한 미루는 코드 중 어떤 게 성능 측면에서 좋을지 생각해보면 답은 쉽게 나온다. 그렇기 때문에 지연 초기화를 쓸 경우 소프트웨어의 실행 시간 및 메모리 효율(공간)을 개선할 수 있다

 

https://blog.logrocket.com/initializing-lazy-lateinit-variables-kotlin/

 

Initializing lazy and lateinit variables in Kotlin - LogRocket Blog

This article will explain how the lateinit modifier and lazy delegation can take care of unused or unnecessary early initializations.

blog.logrocket.com

코틀린은 일반적으로 속성을 정의하는 즉시 속성을 초기화하게 요구한다. 특히 생명주기 기반 안드로이드의 경우 이상적인 초기값을 모를 때 이렇게 하는 게 이상해 보인다. 초기화하지 않고 클래스 속성을 선언하면 IDE에서 경고를 표시하고 lateinit 키워드 추가를 권장한다

 

그 외에 다른 곳에서도 비슷하게 말하기 때문에 생략한다.

어쨌든 지연 초기화라는 걸 쓰면 실행 시간과 메모리 효율 측면에서 이점이 있다는 건 알겠다. 그럼 쓰는 건 어떻게 하는가? 제목에 써 둔 lateinit과 Lazy를 사용하면 된다. 그럼 이 둘은 각각 뭐고 둘의 차이는 뭔가?

 

https://kotlinlang.org/docs/properties.html#late-initialized-properties-and-variables

 

Properties | Kotlin

 

kotlinlang.org

일반적으로 null이 아닌 타입을 갖는 것으로 선언된 속성은 생성자에서 초기화돼야 한다. 그러나 그렇게 하는 게 불편한 경우가 종종 있다. 예를 들어 속성(properties)은 종속성 주입을 통해 초기화하거나 단위 테스트의 설정 메서드에서 초기화할 수 있다. 이런 경우 생성자에서 null이 아닌 이니셜라이저를 제공할 수 없지만 클래스 본문 안에서 속성을 참조할 때 여전히 null 체크를 피하려고 한다. 이 경우를 위해 lateinit 한정자로 속성을 표시할 수 있다
public class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        subject = TestSubject()
    }

    @Test fun test() {
        subject.method()  // dereference directly
    }
}
이 한정자는 최상위 수준 속성 및 지역 변수 뿐 아니라 클래스 본문 안에 선언된 var 속성에서(기본 생성자가 아닌 속성에 커스텀 getter 또는 setter가 없는 경우에만) 쓸 수 있다. 속성 또는 변수의 타입은 null이 아니어야 하며 기본 타입이 아니어야 한다. 초기화되기 전에 lateinit 속성에 접근하면 접근되는 속성과 초기화되지 않았다는 사실을 명확하게 식별하는 특수 예외가 발생한다

< lateinit var가 초기화됐는지 확인하기 >

lateinit var가 이미 초기화됐는지 확인하려면 해당 속성의 참조에 ".isInitialized"를 사용한다
if (foo::bar.isInitialized) {
    println(foo.bar)
}
이 검사는 동일한 타입, 외부 타입 중 하나 또는 같은 파일의 최상위 수준에서 선언될 때 어휘적으로(lexically) 접근 가능한 속성에 대해서만 쓸 수 있다

 

중요한 사실은 lateinit은 var 타입에만 쓸 수 있다는 것이다. 그래서 IDE에서 "la"만 입력하면 자동으로 "lateinit var"를 선택할 수 있다.

 

 

만약 var를 지우고 val을 억지로 쓰면 컴파일 에러가 발생한다.

 

 

저 3가지 에러의 뜻은 각각 아래와 같다.

 

  • 'lateinit' 한정자는 변경 가능한 속성에서만 허용된다
  • 'lateinit' 한정자는 기본 타입의 속성에선 허용되지 않는다
  • 'lateinit' 한정자는 이니셜라이저가 있는 속성에서 허용되지 않는다

 

그리고 null을 허용하지 않는 자료형에만 사용가능하다. 이건 직접 IDE에 타이핑해보면 확인 가능하다.

 

 

이제 맨 위의 name 속성을 선언해두고 메인에서 사용해 본다.

 

lateinit var name: String

fun main() {
    println(name)
}
// Exception in thread "main" kotlin.UninitializedPropertyAccessException:
// lateinit property name has not been initialized

 

lateinit var를 선언만 해두고 값을 넣지 않았기 때문에 UninitializedPropertyAccessException이란 예외가 발생하는 걸 볼 수 있다. 빨간 줄이 발생하지도 않아서 처음 쓸 때 실수하기 좋다.

name에 아무 문자열이나 넣고 다시 실행하면 잘 작동하는 걸 볼 수 있다. 기왕 쓰는 김에 초기화됐는지 확인하는 함수도 사용해 본다.

 

lateinit var name: String

fun main() {
    name = "철수"
    if (::name.isInitialized) {
        println("name : $name")
    } else {
        println("name 변수가 초기화되지 않았습니다")
    }
}

 

isInitialized 함수는 사용법이 특이한 걸 볼 수 있다. 콜론 2개가 name 앞에 붙었는데, 이렇게 쓰는 이유는 isInitialized 함수 선언 시 속성이 가지는 값이 아니라 참조를 전달해야 하기 때문에 참조임을 나타내는 콜론 2개를 써야 한다.

이와 관련된 코틀린 공식문서는 아래와 같다.

 

https://kotlinlang.org/docs/reflection.html#property-references

 

Reflection | Kotlin

 

kotlinlang.org

코틀린에서 1급 객체로 속성에 접근하려면 "::" 연산자를 사용하라
val x = 1

fun main() {
    println(::x.get())
    println(::x.name)
}
"::x" 표현식은 KProperty<Int> 유형 속성 객체로 평가된다. get()을 써서 해당 값을 읽거나 name 속성을 이용해 속성 이름을 검색할 수 있다. var y = -1 같은 가변 속성의 경우 "::y"는 set()이 있는 KMutableProperty<Int> 타입의 값을 반환한다

 

갑자기 1급 객체란 어려운 단어가 튀어나온다. 이 단어가 나오는 이유는 코틀린에서 함수와 속성(properties)은 일급 시민으로 취급받기 때문이다.

자세한 내용은 이 포스팅의 범위를 넘기 때문에 생략하고, 위 링크의 키워드인 Reflection에 대해 알아보면 좋을 것 같다.

위에서 String이 사용 가능했으니 ArrayList, HashMap 등의 컬렉션에도 lateinit var 한정자를 사용할 수 있다.

 

lateinit var list: ArrayList<Int>
lateinit var map: HashMap<Int, Int>
lateinit var set: HashSet<Int>

fun main() {
    list = arrayListOf()
    map = hashMapOf()
    set = hashSetOf()
    
    list.add(1)
    map[0] = 1
    set.add(1)
    println("list : $list, map - ${map.keys} : ${map.values}, set : $set")
}
// list : [1], map - [0] : [1], set : [1]

 


 

다음은 Lazy에 대해 알아본다.

 

https://kotlinlang.org/docs/delegated-properties.html#lazy-properties

 

Delegated properties | Kotlin

 

kotlinlang.org

코틀린 표준 라이브러리는 몇 가지 유용한 종류의 대리자(delegates)를 위한 팩토리 메서드를 제공한다. lazy()는 람다를 취하고 지연 속성(lazy property)을 구현하기 위한 대리자 역할을 할 수 있는 Lazy<T>의 인스턴스를 반환하는 함수다. get()에 대한 1번째 호출은 lazy()에 전달된 람다를 실행하고 결과를 기억한다. get()에 대한 후속 호출은 단순히 기억된 결과를 반환한다
val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main() {
    println(lazyValue)
    println(lazyValue)
}
// computed!
// Hello
// Hello
기본적으로 게으른 속성(lazy properties)의 평가는 동기화된다. 값은 하나의 쓰레드에서만 계산되지만 모든 쓰레드는 동일한 값을 보게 된다. 여러 쓰레드가 동시에 실행할 수 있도록 초기화 대리자(initialization delegate)의 동기화가 불필요한 경우 LazyThreadSafeMode.PUBLICATION을 매개변수로 lazy()에 전달한다. 속성을 사용하는 쓰레드와 동일한 쓰레드에서 초기화가 항상 발생한다고 확신하는 경우 LazyThreadSafeMode.NONE을 쓸 수 있다. 쓰레드 안전성 보장 및 관련 오버헤드가 발생하지 않는다

 

https://medium.com/@ankit.sinhal/kotlin-lazy-vs-lateinit-properties-when-to-use-which-property-97173c2e55ff

 

Kotlin Lazy vs Lateinit Properties. When to use which property?

Kotlin provides many great features. We can leverage these features and build a high-quality application quickly. Among all those features…

medium.com

실제로 필요할 때까지 객체 생성, 초기화를 미루는 것은 소프트웨어 공학에서 일반적인 패턴이다. 이 패턴은 지연 초기화로 알려져 있으며 앱 시작 중에 많은 객체를 할당하면 시작 시간이 길어질 수 있으므로 특히 안드로이드에서 일반적이다...(중략)...코틀린에서 지연 초기화는 언어의 일부다. 지시문 by lazy를 사용하고 초기화 블럭을 제공하면 지연 인스턴스화의 나머지 부분이 암시적이다
class MyClass {
    val heavy by lazy { // Initialization block
        HeavyObject()
    }
}
기본적으로 위 코드는 쓰레드 세이프하다. MyClass::getHeavy()에 대한 호출은 한 번에 한 쓰레드만 초기화 블럭에 있도록 동기화된다. 지연 초기화 블럭에 대한 동시 접근의 세밀한 제어는 LazyThreadSafeMode를 써서 관리할 수 있다. 코틀린 지연 값은 런타임에 호출될 때까지 초기화되지 않는다. 무거운 속성이 처음 참조될 때 초기화 블럭이 실행된다

 

이처럼 lateinit과 Lazy는 모두 지연 초기화라는 같은 목적을 위해 존재하는 키워드지만 사용법과 주의사항이 각각 다르다. 쓴다면 사용법과 값을 넣었는지 확인하는 절차가 필요하다.

아래는 lateinit, Lazy의 특징들을 정리한 미디엄 포스팅이다.

 

https://medium.com/kenneth-android/kotlin-kotlin-lazy-initialization-%EC%B4%88%EA%B8%B0%ED%99%94-%EC%A7%80%EC%97%B0-57b08f0f6860

 

[Kotlin] Kotlin Lazy Initialization(초기화 지연)

이번 포스팅 에서는 늦은 초기화(Lazy Initialization)에 대해서 알아보겠습니다

medium.com

lateinit
- var 타입만 가능
- non-null만 가능
- primitive type 불가능
- 커스텀 getter / setter 불가능
- 클래스 생성자 인자로 사용 불가능
- 지역 변수로 불가능

Lazy
- val 타입만 가능
- non-null 또는 null 모두 가능
- primitive type 가능
- 커스텀 getter / setter 불가능
- 클래스 생성자 인자로 사용 불가능
- 지역 변수로 불가능

 

실제로 lateinit, by lazy로 변수를 만들고 커스텀 게터세터를 만들려고 하면 컴파일 에러가 발생한다.

아래는 lateinit var 선언 후 커스텀 게터를 만들려고 했을 때 발생하는 에러다. 세터를 만들었을 때도 동일한 에러가 표시된다.

 

 

다음은 by lazy를 사용했을 때 커스텀 게터를 정의하면 발생하는 에러다.

 

 

커스텀 세터의 경우 아래 에러가 발생한다.

 

 

왜 커스텀 게터세터를 사용할 수 없을까?

lateinit var는 초기화되기 전에 접근하면(=사용하려고 하면) UninitializedPropertyAccessException을 내뿜는다. 그리고 lateinit property has not been initialized 에러가 로그캣에 표시된다.

또한 이 lateinit var 변수의 초기화 상태 관리는 코틀린 컴파일러기 때문에 lateinit var 사용 시 커스텀 게터세터를 사용할 수 없게 된다. 그래서 사용하려고 하면 컴파일 에러가 발생하는 것이다.

 

by lazy의 경우 앞서 val 프로퍼티만 사용 가능하다고 했다. val 변수의 특징인 읽기 전용이라는 것만 생각하면 왜 커스텀 세터 설정이 불가능한지 알 수 있다. 읽기 전용 변수기 때문에 한 번 초기화되면 값이 바뀌지 않기 때문에 커스텀 세터가 없는 것이다.

또한 초기화 로직은 Lazy 인터페이스가 관리하기 때문에 커스텀 게터를 정의하려면 이 인터페이스의 동작을 변경해야 한다.

반응형
Comments