HOME > kotlin > basic

Kotlin - lateinit과 Delegates.notNull의 차이점

JSFollow04 Jul 2019

lateinit과 Delegates.notNull는 초기화를 늦추고 객체에 null이 들어가지 않게 합니다. 비슷해보이지만 여러가지 차이점이 있습니다. 또한 내부적인 구현이 다르기 때문에 성능차이가 발생할 수도 있습니다. 이 글에서 이 둘의 특징과 구현의 차이점에 대해서 알아보겠습니다.

lateinit 소개

Kotlin - 프로퍼티 초기화를 지연하는 방법, Late init과 Lazy init 소개에서 설명한 것처럼 프로퍼티의 초기화를 늦게 하는데 도와주는 기능입니다. 여기서 간단히 특징에 대해서 나열해보겠습니다. 자세한 것은 위의 글을 참고해주세요.

  • var 프로퍼티만 사용할 수 있음
  • primitive type(Int, Boolean)은 사용할 수 없음
  • 프로퍼티에 null을 넣을 수 없음
  • Custom getter/setter를 만들 수 없음
  • 초기화 전에 사용하면 UninitializedPropertyAccessException이 발생

lateinit 내부 구현

아래는 Kotlin - 프로퍼티 초기화를 지연하는 방법, Late init과 Lazy init 소개에서 소개한 예제 코드입니다.

class Rectangle {
    lateinit var area: Area
    fun initArea(param: Area): Unit {
        this.area = param
    }
}

class Area(val value: Int)

fun main() {
    val rectangle = Rectangle()
    rectangle.initArea(Area(10))
    println(rectangle.area.value)
}

위 코드를 자바로 디컴파일해보면 다음과 같습니다. (일부 생략하였고 전체코드는 위 글을 참고해주세요.) 객체가 처음 null로 설정되기 때문에, 초기화를 하지 않고 사용하면 예외가 발생합니다.

public final class Rectangle {
   @NotNull
   public Area area;

   @NotNull
   public final Area getArea() {
      Area var10000 = this.area;
      if (var10000 == null) {
         Intrinsics.throwUninitializedPropertyAccessException("area");
      }

      return var10000;
   }
   ...
}

public static final void main() {
   Rectangle rectangle = new Rectangle();
   rectangle.initArea(new Area(10));
   int var1 = rectangle.getArea().getValue();
   System.out.println(var1);
}

Delegates.notNull() 소개

Delegates.notNull()는 객체를 non-null 타입으로 만들어버립니다. 그래서 var로 선언하였지만 객체에 null을 할당할 수는 없습니다. 대신 null이 아닌 객체를 할당할 수 있습니다.

우리는 Delegates.notNull()을 아래 코드처럼 사용할 수 있습니다. 객체를 선언할 때 뒤에 by Delegates.notNull<타입>()를 붙여주면 됩니다. 만약 nonNullString = null처럼 null을 입력하려고 하면 컴파일이 에러가 발생합니다.

var nonNullString: String by Delegates.notNull<String>()
nonNullString = "Hello World"
println("Non null value is: ${nonNullString}")
nonNullString = null  // 컴파일 에러, non-null 타입에 null을 넣을 수 없음

만약 초기화를 하지 않고 객체를 사용하면, IllegalStateException이 발생합니다.

var nonNullString: String by Delegates.notNull<String>()
println("Non null value is: ${nonNullString}")

이런 식으로요. 초기화되지 않은 객체를 사용할 때 예외가 발생하는 것은 lateinit과 동일합니다. 또한 null을 넣을 수 없다는 것이 동일합니다.

Exception in thread "main" java.lang.IllegalStateException: Property nonNullString should be initialized before get.
	at kotlin.properties.NotNullVar.getValue(Delegates.kt:62)
	at foo.main.kotlin.Kotlin23Kt.main(kotlin23.kt:29)

간단히 Delegates.notNull()의 특징을 정리해보겠습니다.

  • primitive type 도 사용할 수 있다
  • 초기화 전에 사용하면 예외가 발생한다
  • null을 설정하려고 하면 컴파일 에러가 발생한다
  • late init보다 성능이 안좋다(다음에 설명)

Delegates.notNull() 내부 구현

아래 코드는 위에서 사용한 예제입니다.

var nonNullString: String by Delegates.notNull<String>()
nonNullString = "Hello World"
println("Non null value is: ${nonNullString}")

위 코드를 자바로 변환해보면 다음과 같습니다.

public final class Test {
  static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)
      Reflection.mutableProperty0(new MutablePropertyReference0Impl(
        Reflection.getOrCreateKotlinPackage(
          Test.class, "kotlin"), "nonNullString", "<v#0>"))};

   public static final void main(@NotNull String[] args) {
      ReadWriteProperty nonNullString = Delegates.INSTANCE.notNull();;
      KProperty var3 = $$delegatedProperties[0];
      nonNullString.setValue((Object)null, var3, "Hello World");
      String var4 = "Non null value is: " + (String)nonNullString.getValue((Object)null, var3);
      System.out.println(var4);
   }
}

디컴파일된 코드를 보시면 코틀린에서 String 타입으로 Delegates.notNull을 적용하였는데요. 디컴파일해보니 타입이 ReadWriteProperty로 변해있습니다. 따라서, lateinit처럼 코드적으로 예외처리를 하는 것이 아니라 ReadWriteProperty 내부에 자신이 선언한 타입을 감싸고(wrapping) 있는 구조입니다. 그렇기 때문에 primitive type도 사용할 수 있습니다. 반면에 lateinit은 사용할 수 없습니다.

lateinit과 Delegates.notNull()의 차이점

이 둘의 내부 구현 원리를 안다면 차이점에 대해서 쉽게 이해할 수 있습니다. Delegates.notNull은 특정 객체가 선언한 객체를 감싸기 때문에 primite type도 사용할 수 있습니다. 하지만 lateinit은 선언한 타입을 사용하고 초기값을 null로 설정하기 때문에 primitive type은 사용할 수 없습니다. 이런 구조적인 차이 때문에 Delegates.notNull가 오버헤드가 클 수 밖에 없습니다. 만약 많은 데이터를 처리해야 한다면 이런 구조에서 오는 성능상의 차이가 발생할 수 있습니다.

  • Delegates.notNull은 primitive type도 사용할 수 있지만 lateinit은 사용할 수 없다
  • primitive type을 사용하려면 Delegates.notNull을 사용해야 한다
  • Delegates.notNull가 lateinit보다 더 무겁게 동작한다
  • Delegates.notNull는 Dagger와 같은 injection 라이브러리와 함께 쓰기 어려울 수 있다
  • lateinit은 custom getter/setter를 만들 수 없다.

참고