본문 바로가기
오늘은 뭘 배울까?/Android

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

by Kim Juhwan 2022. 6. 22.

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

 

 

 


 

 

1. 요약

 

lateinit과 by lazy를 아시나요?
둘의 차이는 무엇인가요?

 

lateinit과 by lazy는 늦은 초기화를 할 때 사용합니다.
다만 lateinit은 var로 선언해야 하며 언제든 값을 수정할 수 있지만
by lazy는 val로 선언해야 하므로 한 번 초기화를 하면 값을 변경할 수 없습니다.
또, 초기화 시점에 차이가 있습니다.
lateinit은 선언 이후 아무때나 초기화를 할 수 있지만
by lazy는 변수를 호출할 때, 최초 한 번만 초기화를 할 수 있습니다.

 

2. 늦은 초기화란?

String a; // Java는 이렇게 선언만 하는 것이 가능하지만

var b: String // Kotlin은 이렇게 선언만 하는 것이 불가능하다.

Java에서는 초기화 없이 선언만 하면 변수에 기본으로 값이 들어간다.

예를 들어 위 코드의 경우 a에 null이 들어간다.

 

하지만 Kotlin의 경우는 초기화가 필수이기 때문에

저렇게 적으면 b에 빨간줄이 찍~ 그어진다.

 

var b: String? = null

그렇다면 이렇게 문제를 해결해볼수도 있을 것이다.

String을 nullable 타입으로 만들고 null로 초기화하는 것이다.

물론 이렇게 하면 원하는 대로 실행은 할 수 있을 것이다.

하지만 null 사용을 피하자는 Kotlin 언어의 의도를 무참히 짓밟는... (?) 행위라고 볼 수 있다.

 

그래서 이때 사용하는 것이 바로 늦은 초기화이다.

"있잖아, 내가 b라는 변수에 당장 무슨 값을 넣어야 할지 모르겠는데 말이야. 초기화를 하긴 할 거거든? 대신 조금 늦게 할할게"라고 약속을 하는 게 늦은 초기화이다.

약속을 했으니 null로 초기화를 해주지 않아도 된다.

 

늦은 초기화를 하는 방법에는 2가지가 있는데 지금부터 알아보자.

 

3. lateinit

lateinit var a: string

a = "이렇게 나중에 초기화가 가능하단 말이죵"

println(a)

우선 첫 번째로 lateinit이다.

변수를 선언할 때 맨 앞에 키워드를 달아주면 된다.

lateinit의 경우 언제든지, 몇 번이든 초기화가 가능해야 하므로 항상 var 키워드와 같이 사용한다.

만약 lateinit과 val을 같이 사용하면...

 

lateinit: 지금 말고 나중에 초기화할게요 ^^

val: 지금 초기화하고 나중에는 안 할게요 ^^

 

초기화를 언제 하겠다는겨

 

두 키워드가 정반대의 의미를 가지기 때문에 lateinit과 val 키워드는 같이 쓰일 수 없다.

 

4. by lazy

lateinit idol: String -------------- 1번

var introduce: String by lazy { -------------------- 2번
	"제 요즘 최애는" + idol + "이었습니다 짜잔"
}

idol = "송하영" -------------------- 3번

println(introduce) ----------------- 4번

lateinit은 맨 앞에다가 키워드를 달아줬다면

by lazy는 맨 뒤에다가 키워드를 달아주면 된다.

 

어떤 상황에서 쓰는 건지 이해를 해보자.

지금 2번 라인을 보면 idol 변수를 사용해서 introduce 변수에 넣으려고 하고 있다.

하지만 3번 라인까지 실행이 되어야 idol에 무슨 값이 들어있는지 아는 상황!!

즉, 늦은 초기화가 필요하다.

by lazy가 초기화되는 시점은 바로 4번 라인이 실행됐을 때이다.

변수가 최초로 사용되는 그 시점에 초기화가 진행된다.

 

그렇게 by lazy를 사용해서 "제 요즘 최애는 송하영이었습니다 짜잔"이라는 결과가 나오는 것이다.

그래서 송하영이 누구냐구요?

 

 

이런 친구가 있는데 귀엽더라구요...

안 물어봤으니 계속 설명이나 계속 하라구요?

알겠습니다 😢

 

5. 정리

  lateinit by lazy
초기화 횟수 제한 없음 최초 한 번만
초기화 시점 아무때나 변수 호출 시
같이 사용하는 키워드 var val
Primitive Type 사용 가능? X O

 

정리해보자면 위 표와 같다.

Primitive Type은 Int, Float, Double, Long 등과 같은 타입을 말한다.

 

6. 추가로 나올 수 있는 질문

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

 

Ctrl + Shift + A를 눌러 Kotlin Bytecode를 검색

 

kotlin 코드를 Java 코드로 Decompile 해보면 정답을 알 수 있다.

안드로이드 스튜디오를 열고 위 사진처럼 검색을 해보자.

 

디컴파일할 클래스 파일 선택 - Decompile 버튼 클릭

그다음 위 사진의 순서대로 클릭을 해주면...

 

디컴파일된 Java 코드

 

Kotlin으로 되어있던 코드를 Java 코드로 볼 수 있다.

여기서 lateinit 부분이 어떻게 생겼나 보면 된다.

 

   public static final sampleAdapter access$getSampleAdapter$p(SampleActivity $this) {
      SampleAdapter var10000 = $this.sampleAdapter;
      if (var10000 == null) {
         Intrinsics.throwUninitializedPropertyAccessException("sampleAdapter");
      }

      return var10000;
   }

이런 식으로 생겼다.

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

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

 

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

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

 

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

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

 

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

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

 

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

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

 

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

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

 

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

[목차 5]에서 언급했듯이 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 static final sampleAdapter access$getSampleAdapter$p(SampleActivity $this) {
      SampleAdapter var10000 = $this.sampleAdapter;
      if (var10000 == null) {
         Intrinsics.throwUninitializedPropertyAccessException("sampleAdapter");
      }

      return var10000;
   }

[목차 6-1]에서 까 봤던 lateinit의 내부 코드이다.

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

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

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

 

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

 

 


💡 느낀 점

  • lateinit이랑 Primitive를 같이 사용할 수 없는 이유를 몰랐는데 이번 기회에 알게 됐다. 코드 까보는 거... 생각보다 재밌을지도...?
  • 생각해보면 Kotlin이 Java 기반 언어니까 변수를 초기화하지 않으면 값에 null이 들어있다는 것이 어쩌면 당연한 것 같다. lateinit을 사용하면 초기화 전에 뭔가 특별한 값이나 로직이 있을 거라 생각한 나 반성해...
  • lateinit이 단순 if문 처리로 이루어진 로직이었다니!! 고작 6줄짜리..!
  • by lazy의 by 키워드도 제대로 공부를 해봐야겠다. by lazy가 하나의 키워드가 아니고 lazy를 by 하는 거니까... 정확히 어떻게 돌아가는 건지 궁금하다.
  • 송하영은 귀엽다

📘 참고한 자료


 

 

반응형

댓글