본문 바로가기
Kotlin

[Kotlin] 늦은 초기화 (lateinit, by lazy)

by 태크민 2023. 8. 22.

늦은 초기화 기법

늦은 초기화 라 함은, 말 그대로 객체 초기화를 늦게 하는 것이다.

예를 들어 분명 변수 a를 사용할 예정인데, a의 첫 상태를 정의하기 어려울 때 어떻게 하겠는가?

그럼 우린 이렇게 할 수 있다.

var a: String? = null

그치만 어차피 이후에 분명 사용할 녀석인데 굳이 위험하게 초기 상태로 null을 사용해야할까?

null 사용의 지양을 강조하는 코틀린 창시자들은 분노할 것이 뻔하다..

 

 

왜 늦은 초기화를 쓰는가?

  • Null 안정화
    • 코틀린은 기본적으로 null 값을 허용하지 않으므로, 해당 프로퍼티가 초기화된 이후에만 접근할 수 있도록 처리하는 것이 좋습니다. 늦은 초기화를 사용하면 nullable 프로퍼티를 사용할 필요가 없어지므로, 불필요한 null 처리를 줄일 수 있습니다.
  • 메모리 공간 효율 개선 (Memory Leak 방지):
    • 지연 초기화는 객체가 실제로 필요할 때까지 메모리에서 할당되지 않기 때문에, 불필요한 객체 생성을 피할 수 있습니다. 예를 들어, 클래스의 특정 프로퍼티가 항상 사용되지 않는 경우, 지연 초기화를 통해 메모리 사용을 최소화할 수 있습니다. 이를 통해 메모리 누수를 방지하고, 전체 애플리케이션의 메모리 효율성을 높일 수 있습니다.
  • 성능 향상 (초기화 지연으로 실행시간 향상):
    • 초기화가 필요한 시점까지 프로퍼티를 생성하지 않기 때문에, 프로그램의 시작 시간이나 초기 실행 시간이 단축될 수 있습니다. 예를 들어, 대용량 데이터나 무거운 객체를 초기화할 필요가 없을 때, 실제로 사용될 때까지 기다림으로써 성능을 개선할 수 있습니다. 이렇게 함으로써 앱의 반응성이 향상되고, 사용자가 느끼는 지연을 줄일 수 있습니다.

 

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

예를 들면 viewModel은 onCreate에서 뷰가 그려지기 전까지는 메모리만 차지하고 해당 역할은 아무것도 하지 않습니다. 일찍 초기화할 이유가 없습니다.

 

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

 

lateinit

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

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

 

일단 코드부터 살펴보자

lateinit var text: String

fun main() {  

    // 대충 중간에 뭔가 했음
    val result1 = 30

    text = "Result : $result1"
    println(text)

    // 대충 뭔가 또 했음
    val result2 = 50

    text = "Result : ${result1 + result2}"
    println(text)
}

 lateinit을 사용하여 text 변수를 선언해줬고, 이후에 어떤 동작의 결과 값을 기반으로 text를 초기화 해주는 것을 확인할 수 있다.

이후에 또 한번 값을 바꾸는 것을 확인할 수 있는데, lateinit 변수 선언부를 자세히 보면 var로 선언되어 있다. lateinit을 사용하면 늦은 초기화 이후에도 값이 계속하여 바뀔 수 있다.

lateinit는 계속하여 값이 변경될 수 있다는 속성을 위해 무조건 var을 사용해야 하며, Primitive Type (Int, Float, Double, Long 등)에는 사용할 수 없다.

 

그럼, 만약 lateinit을 사용해놓고 늦은 초기화조차 하지 않은 경우는 어떻게 될까?

아래와 같은 에러를 볼 수 있을 것이다.

Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property text has not been initialized

 

by lazy

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

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

마찬가지로 코드를 먼저 살펴보도록 하자.

lateinit var text: String
val textLength: Int by lazy {
    text.length
}

fun main() {   
    // 대충 중간에 뭔가 했음
    text = "H43RO_Velog"
    println(textLength)
}

모양새를 by lazy 구문을 통해 어떤 생성자를 넣어준 모양이다.그런데 자세히 보면 lateinit이라서 아직 초기화가 되지 않은 text의 속성을 활용한 모습을 볼 수 있다.

 

by lazy는 선언 당시에는 초기화를 할 방도가 없지만, 이후에 의존하는 값들이 초기화 된 이후에 값을 채워넣고 싶을 때 사용한다. 즉, 호출 시에 이를 어떻게 초기화를 해줄 지에 대하여 정의할 수 있는 구문이다.

 

따라서 text가 초기화 된 이후에 textLength를 출력할 때 text.length 속성을 사용하여 textLength라는 변수를 초기화 할 수 있다.

 

선언부를 자세히 보면, val로 선언이 되어 있는 것을 볼 수 있다. 이는 단 한번의 늦은 초기화가 이루어지고 이후에는 값이 불변함을 보장한다.

💡 Tip
안드로이드에선 이전 액티비티에서 넘어온 Intent Bundle Extra 등을 현재 액티비티 멤버 변수에 by lazy 로 받아와, 선언해두고 사용 시에 intent.extra 등으로 번들을 뜯어볼 수 있다. 이렇게 하면 생명주기를 위반하지 않고 안전하게 클래스 전역에서 사용할 수 있는 값을 갖고올 수 있다.

Lazy 동기화

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

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

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

 

 

한눈에 비교하기

  • 값 변경
    • lateinit: 가능 (var 사용)
    • by lazy: 불가능 (val 사용)
  • 용법 구분
    • lateinit: 초기화 이후에 계속하여 값이 바뀔 수 있을 때
    • by lazy: 초기화 이후에 읽기 전용 값으로 사용할 때

 

추가로 나올 수 있는 질문

1. lateinit을 초기화하기 전 까지는 변수에 무슨 값이 들어있을까요?

아래 코드를 디컴파일 해보도록 하겠습니다.

lateinit var text: String
class cctest {
    
}

 

 

디컴파일된 Java 코드

public final class CctestKt {
   public static String text;

   @NotNull
   public static final String getText() {
      String var10000 = text;
      if (var10000 == null) {
         Intrinsics.throwUninitializedPropertyAccessException("text");
      }

      return var10000;
   }

   public static final void setText(@NotNull String var0) {
      Intrinsics.checkNotNullParameter(var0, "<set-?>");
      text = var0;
   }
}

이런 식으로 생겼다.

var10000이라는 변수가 null이면 Exception을 발생시키고 값이 있으면 그대로 리턴한다.

즉, lateinit을 사용하면 초기화하기 전까지 변수에 null이 들어간다는 사실!!

 

사실 나는 이걸 알아보기 전까지 "변수에 null 말고 다른 값이 들어가 있겠지"라고 생각했다.

왜냐하면 null을 허용하기 위해서 lateinit을 사용하니까!

 

👨‍💻 코틀린 개발자 : 너네한테 null 사용 지양하라고 했지 내가 안 쓴다곤 안 함 ㄹㅇㅋㅋ

약간 이런 느낌..? 배신당한 느낌이 들었다.

 

뭐 암튼 lateinit은 그래서 까본 결과 뭐 대단한 기술이 들어간 게 아니라

null이면 Exception을 발생시켜 컴파일 단계에서 개발자가 초기화가 안됐음을 자각할 수 있도록 해주는 키워드였다.

 

2. 초기화를 하지 않고 실행하면 어떻게 될까요?

https://developer.android.com/kotlin/common-patterns

 

[목차 1]을 제대로 읽었거나 평소에 초기화를 빼먹는 실수를 많이 했다면 알 수 있는 답이다.

컴파일 단계에서 Exception이 발생한다.

 

3. 왜 lateinit은 Primitive Type과 사용할 수 없을까요?

앞서 언급했듯이 lateinit은 Primitive Type에 사용할 수 없고

by lazy는 모든 타입에 사용 가능하다.

 

 

음.. 왜일까?? 왜??

Primitive Type을 사용하지 못하는 건 알겠는데 그 이유를 알아야겠다 싶어서 조사를 해봤다.

var flag: Boolean = true // 이건 가능하지만
 
var flag: boolean = true // 이건 불가능하다

우선, 코틀린은 자바와 달리 Primitive Type과 Wrapper Type이 따로 구분되지 않는다.

변수의 타입을 지정할 때 Boolean은 되는데 boolean이 안 되는 이유가 이를 증명한다.

엥? 아니 그러면 코틀린은 대체 무슨 타입을 사용하는 걸까? 대체 내부가 어떻게 돌아가는 거지?

숫자 타입을 객체로 표현하면 계산할 때 비효율적일 텐데??

 

정답은 그때그때마다 알아서 판단해서 Primitive 혹은 Wrapper 타입으로 변환한다.

예를 들어 Collection이나 Generic을 사용하는 경우는 Wrapper로 변경되고

그 외 나머지 경우는 Primitive로 변경된다.

 

자, 좋다. 그래 그럼 코틀린이 타입이 구분되지 않는다는 건 알겠다.

그럼 이게 lateinit이 Primitive를 사용하지 못하는 것과 무슨 관계가 있을까?

public final class CctestKt {
   public static String text;

   @NotNull
   public static final String getText() {
      String var10000 = text;
      if (var10000 == null) {
         Intrinsics.throwUninitializedPropertyAccessException("text");
      }

      return var10000;
   }
   ..
}

위에서 까 봤던 lateinit의 내부 코드이다.

초기화가 됐는지 안됐는지 판단을 null로 하고 있다.

근데 애석하게도 Primitive Type은 null 값을 가질 수 없다.

그렇다면 대체 초기화가 되었는지 안되었는지 무슨 기준으로 판단할 수 있을까?

 

판단할 수 있는 기준이 없기 때문에 lateinit에서 Primitive Type을 지원하지 않는 것이다.


참고자료

https://velog.io/@haero_kim/Kotlin-lateinit-vs-lazy-%EC%A0%95%ED%99%95%ED%9E%88-%EC%95%84%EC%84%B8%EC%9A%94

 

[Kotlin] 🤚🏻 lateinit vs lazy, 정확히 아세요?

조금이라도 헷갈린다면 들어오세요!

velog.io

 

[Kotlin] Kotlin Lazy Initialization (초기화 지연) | by Kenneths | Kenneth Android | Medium

 

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

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

medium.com

lateinit과 by lazy의 차이가 무엇인가요?

 

lateinit과 by lazy의 차이가 무엇인가요?

1. 요약 2. 늦은 초기화란? 3. lateinit 4. by lazy 5. 정리 6. 추가로 나올 수 있는 질문 6-1. lateinit을 초기화하기 전 까지는 변수에 무슨 값이 들어있을까요? 6-2. 초기화를 하지 않고 실행하면 어떻게 될

todaycode.tistory.com