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

Kenneths
Kenneth Android
Published in
5 min readSep 16, 2020
Image by Kenneth

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

객체 지향 패러다임 에서는 늦은 초기화는 중요한 행위입니다

왜 지연초기화를 쓰는가?(Why Lazy Initialization)

  • 메모리 공간 효율 개선(Memory Leak 방지)
  • 성능향상(초기화 지연으로 실행시간 향상)

위 2가지 장점 만으로도 지연 초기화를 사용하지 않을 이유는 없어 보입니다

kotlin의 프로퍼티 지연 초기화는 lateinit, lazy 2가지가 있습니다

lateinit

초기화 지연 프로퍼티로써 초기화를 나중으로 미루기 위해 사용합니다.
프로퍼티에 선언하여 사용하고 다음과 같은 제약조건이 있습니다.

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

왜 lateinit을 쓸까? (Why use a lateinit?)

첫번째는 보통 non-null type 으로 선언된 프로퍼티는 생성자함수에서 초기화하는게 일반적이지만 비효율적입니다.

여기서 비효율적인 것은 객체가 인스턴스화 될때 멤버변수의 값을 생성자를 통해 할당하게 되는데 생성자를 통한 할당이 아닌 값의 할당이 일어나기 바로 직전까지 초기화를 지연시키는 것이 lateinit의 목적이라고 할 수 있습니다.

두번째는 null check입니다. 만약 프로퍼티에 null을 할당해 놓고 쓰게된다면 해당 프로퍼티에 접근시 null check를 항상 해주어야 한다는 문제가 있습니다.

만약 lateinit 프로퍼티의 초기화를 하지 않고 접근하게 되면 어떻게 될까요?

onCreategetAdapter함수에서 var10000 변수에 접근시 null일 때
throwUninitializedPropertyAccessException을 발생시킵니다.

그럼 안드로이드에서 초기화를 지연하면서 null이 아닌 프로퍼티 할당은 어디에 쓰일까요?

그렇습니다.(?) 답은 Dagger와 UnitTest에 있습니다.

각 프로퍼티는 멤버변수에 즉시 할당이 아니라 onCreate, setup함수에서 초기화를 해주는것이 퍼포먼스 측면에서 좋습니다.
예를 들면 viewModel은 onCreate에서 뷰가 그려지기 전까지는 메모리만 차지하고 해당 역할은 아무것도 하지 않습니다. 일찍 초기화할 이유가 없습니다.

‘lateinit 프로퍼티는 확실하게 초기화 지연을 보장하는게 안전합니다.
하지만 어떠한 이유로 초기화가 되어야 하는지 확인해야 할 때가 있습니다. 그럴 때는 isInitialized를 사용할 수 있습니다.

if (::adapter.isInitialized) {
println()
}

Lazy

by lazy 는 읽기 전용(Read-Only) 프로퍼티를 유용하게 사용할 수 있게 합니다. by lazy{ } 의 코드 초기화 블록은 프로퍼티가 최초로 사용 될때 해당 블록이 실행됩니다. 그리고 다음과 같은 제약 조건이 있습니다

  • val 타입만 가능
  • non-null or null 둘다 가능
  • primitive type 가능
  • Custom getter/setter 불가능
  • 클래스 생성자 인자로 사용 불가능
  • 지역 변수로 불가능

이제 by lazy 를 설명하기 전에 lateinit 을 다시 찾아봅시다.
안드로이드 액티비티에서 뷰의 인스턴스를 다른 함수에서 변경하기 위해
멤버변수에 할당하고사용하는 경우가 많이 있습니다.

아래와 같이 초기화를 한다고 가정해보겠습니다.

이상한점을 발견하셨나요? 맞습니다.(?)
첫번째로 onCreate 함수에서 init 함수를 호출하지 않았습니다. 이 부분은 실수 하지 않을것 같지만 실수할 여지가 남는다면 Runtime에러가 발생 할 것입니다.

두번째로 한번 할당된 TextView 인스턴스를 다시 바꿀 이유는 없기 때문에
val 타입으로 할당하는게 좋습니다.

액티비티안에서 라이프사이클 상태와 메인쓰레드에서 뷰를 참조한다고 가정했을때 Runtime에러를 by lazy 를 통해서 방지 할 수 있습니다.

textviewby lazy 코드 블록은 textview객체에 최초 접근
textview.text = "Sample" 해당 코드에서 초기화 블록이 실행됩니다.
정상적인 안드로이드 액티비티 라이프사이클 안에서 초기화 블록이 실행된다면 Runtime에러가 나지않을 뿐더러 불변 변수인 val 으로 뷰의 인스턴스가 바뀔일도 없습니다.

Lazy 동기화

Lazy의 동기화 옵션(LazyThreadSafetyMode)은 3가지다.

  • SYNCHRONIZED : 1개의 스레드만 값을 계산 할 수 있고 모든 스레드가 같은 값을 읽음.
  • PUBLICATION : 초기화를 여러 스레드가 할 수 있고 처음 생성된 인스턴스만 반환한다
  • NONE : 아무 동기화 연산을 하지 않는다. 단일 스레드 추천

이 중 Default로 SYNCHRONIZED를 사용하고 있다.
성능의 순서는 당연히 아무것도하지않는 NONE 부터 PUBLICATION, SYNCHRONIZED 순서이다

SYNCHRONIZED

by lazy의 프로퍼티의 value는 동기화 된다. 내부적으로 살펴보면
해당 프로퍼티의 value@Volatile 어노테이션과 synchronized 블록을 통해 변수의 가시성을 보장해주어 동기화 처리되어 있다

마무리

Kotlin의 초기화 지연은 강력하고 매력적입니다. 특히 by lazy는 안드로이드 라이프사이클과 만났을때 더 효과를 발휘하는것 같습니다. 목적에 맞는 사용과 동기화를 잘 이용한다면 성능적인 측면에서도 상당히 도움이 될 수 있습니다

--

--

Kenneths
Kenneth Android

사용자들에게 편리하고 AweSome UI, UX를 경험해주고 싶은 상위 티어 개발자가 되고싶어 달려가고있는 개발자입니다. 다양한 내용들을 공유하려고 합니다.