Kotlin lazy property - lateinit/lazy 살펴보기



개인 광고 영역

Kotlin에는 늦은 초기화를 위한 방법 2개를 제공한다.

class의 전역에 아래와 같이 값을 생성하면, 클래스가 만들어지는 시점에 변수를 함께 초기화한다.

class 생성과 동시에 변수가 초기화되면, 재 접근시 빠르게 접근이 가능하여 이득을 볼 수 있다.

class SampleActivity {

	private val sampleAdapter: SampleAdapter =
		SampleAdapter(ImageLoaderAdapterViewModel(this@SampleMainActivity, 3))
}

하지만 위 변수를 꼭 사용하는 게 아니라면 오히려 메모리 손해를 볼 수 있다.

필수 요건이 아닌 경우라면 아래와 같이 늦은 초기화가 필요하다.

class SampleActivity {

	private var sampleAdapter: SampleAdapter? = null

	override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_sample_main)

		// 부르는 시점 초기화
    sampleAdapter = SampleAdapter(ImageLoaderAdapterViewModel(this@SampleMainActivity, 3))
	}
}

kotlin에서는 늦은 초기화 시 null을 명시해야 하는데, 꼭 null이 필요하지는 않다.

Java라면 무조건 null에 대한 접근이 가능하여, 언제든 null로 명시할 수 있지만 kotlin에서는 null은 필요한 경우 명시해야 한다.

특히 Java와 함께 사용하거나, null을 통해 명시가 필요한 경우

위의 샘플은 내가 만든 Adapter 초기화하는 코드이다. 굳이 null을 명시하여 아래와 같은 접근을 할 필요성이 많지 않다.

class SampleActivity {

	private var sampleAdapter: SampleAdapter? = null

	override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_sample_main)

		// 부르는 시점 초기화
    sampleAdapter = SampleAdapter(ImageLoaderAdapterViewModel(this@SampleMainActivity, 3))

		sampleAdapter?.addItem()
		sampleAdapter?.notifyDataSetChanged()
	}
}

안전할 순 있지만 저 변수는 null일 필요는 없으며, 코틀린에서의 전역 변수는 초기화가 필요하다.

그러다 보니 아래와 같은 늦은 초기화 2개를 제공하는데 하나씩 살펴보도록 한다.


Late-Initialized properties

Late-Initialized Properties and Variables에서 Unit test을 통해 lateinit을 간단하게 테스트할 수 있다.

먼저 Java에서는 아래와 같이 변수 초기화가 가능하다.

public class SampleActivity extends AppCompatActivity {

  private SampleAdapter sampleAdapter;

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_sample_main);

    sampleAdapter = new SampleAdapter(new ImageLoaderAdapterViewModel(this, 3));
  }
}

위의 sampleAdapter 변수는 onCreate에서 초기화를 하고 있어 이미 늦은 초기화에 해당하며, 언제든 null을 가질 수 있지만 굳이 null을 명시할 필요가 없는 코드이다.

Late-Initialized Properties을 통해 간단하게 접근하는 게 가능하다.

그전에 아래 코드를 활용하려고 하면,

class SampleActivity : AppCompatActivity() {
	private var sampleAdapter: SampleAdapter
}

아주 간단하게 lateinit 오류를 해결하였으며, Nullable이 아닌 변수로 초기화 가능하여, ?을 통해 접근할 필요도 없어진다.

class SampleActivity {

	private lateinit var sampleAdapter: SampleAdapter

	override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_sample_main)

		// 부르는 시점 초기화
    sampleAdapter = SampleAdapter(ImageLoaderAdapterViewModel(this@SampleMainActivity, 3))

		sampleAdapter.addItem()
		sampleAdapter.notifyDataSetChanged()
	}
}

lateinit 조건

lateinit은 꼭 변수를 부르기 전에 초기화 시켜야 하는데 아래와 같은 조건을 가지고 있다.

  • var(mutable)에서만 사용이 가능하다
  • var이기 때문에 언제든 초기화를 변경할 수 있다.
  • null을 통한 초기화를 할 수 없다.
  • 초기화를 하기 전에는 변수에 접근할 수 없다.
    • lateinit property subject has not been initialized
  • 변수에 대한 setter/getter properties 정의가 불가능하다.
  • lateinit은 모든 변수가 가능한 건 아니고, primitive type에서는 활용이 불가능하다(Int, Double 등)

lateinit 초기화 확인하기

kotlin 1.2부터는 lateinit 초기화를 확인 할 수 있다.

실제 값을 사용할 때 lateinit을 한번 체크해줌으로써 안전하게 접근할 수 있따.

이때 아래와 같이 ::을 통해서만 접근이 가능한 .isInitialized을 사용하여 체크할 수 있다.

class SampleActivity {

	private lateinit var sampleAdapter: SampleAdapter

	override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_sample_main)

		// 부르는 시점 초기화
    sampleAdapter = SampleAdapter(ImageLoaderAdapterViewModel(this@SampleMainActivity, 3))

		if (::sampleAdapter.isInitialized) {
			sampleAdapter.addItem()
			sampleAdapter.notifyDataSetChanged()
		}
	}
}

isInitializedHigher-Order functionkotlin extensions을 통해 구현되어 있다.

@SinceKotlin("1.2")
@InlineOnly
inline val @receiver:AccessibleLateinitPropertyLiteral KProperty0<*>.isInitialized: Boolean
    get() = throw NotImplementedError("Implementation is intrinsic")


lateinit 디컴파일

디컴파일한 코드를 통해 lateinit이 어떤 식으로 동작하는지 확인이 가능한데, this.sampleAdapter == null 의 코드를 확인할 수 있다.

@NotNull
public SampleAdapter sampleAdapter;

@NotNull
public final SampleAdapter getSampleAdapter() {
	 SampleAdapter var10000 = this.sampleAdapter;
	 if(this.sampleAdapter == null) {
			Intrinsics.throwUninitializedPropertyAccessException("sampleAdapter");
	 }

	 return var10000;
}

protected void onCreate(@Nullable Bundle savedInstanceState) {
	 super.onCreate(savedInstanceState);
	 this.setContentView(2131361820);
	 this.sampleAdapter = new SampleAdapter(new ImageLoaderAdapterViewModel((Context)this, 3));
	 SampleAdapter var10000 = this.sampleAdapter;
	 if(this.sampleAdapter == null) {
			Intrinsics.throwUninitializedPropertyAccessException("sampleAdapter");
	 }
}

결국 외부에서 getSampleAdapter을 부르던 onCreate에서 this.sampleAdapter을 사용하든 null인 경우 무조건 throwUninitializedPropertyAccessException을 발생시키는 것을 확인할 수 있다.

결국 초기화를 하지 않으면 해당 메소드에 접근하는 것은 불가능하니, 꼭 초기화를 해야 한다


lazy properties

lateinit은 필요할 경우 언제든 초기화가 가능한 Properties였다.

이번엔 생성 후 값을 변경할 수 없는 val(immutable) 정의인 lazy properties을 살펴본다.

lazy 초기화는 기존 val 변수 선언에 by lazy을 추가함으로 lazy {}에 생성과 동시에 값을 초기화하는 방법을 사용한다.

val sampleAdapter: SampleAdapter by lazy {
	SampleAdapter(ImageLoaderAdapterViewModel(this, 3))
}

lazy는 lateinit 과는 반대로 아래의 조건을 가지므로, 상대적으로 편하게 사용이 가능하며, 실수할 일도 줄어든다.

  • 호출 시점에 by lazy 정의에 의해서 초기화를 진행한다.
  • val(immutable)에서만 사용이 가능하다.
  • val이므로 값을 교체하는 건 불가능하다.
  • 초기화를 위해서는 함수명이라도 한번 적어줘야 한다.
  • lazy을 사용하는 경우 기본 Synchronized로 동작한다.

lazy 로그를 통한 초기화 확인하기

lazy가 늦은 초기화라고 하였으니 아래 샘플 코드의 println을 통해 확인할 수 있는데

class ExampleUnitTest {

    private val subject: String by lazy {
        println("subject initialized!!!!")
        "Subject Initialized"
    }

    @Test
    fun test() {
        println("Not Initialized")
        println("subject one : $subject")
        println("subject two : $subject")
        println("subject three : $subject")
    }
}

위의 결과는 아래와 같음을 확인할 수 있다.

코드상 test()에서 out $sample을 출력하는 부분을 코드상 작성하였는데 그때 출력 결과는 subject initialized!!!!subject one : Subject Initialized이 출력되었다.

Not Initialized
subject initialized!!!!
subject one : Subject Initialized
subject two : Subject Initialized
subject three : Subject Initialized

그리고 또다시 불러올 때 subject two : Subject Initialized 만 불리는 걸 확인할 수 있다.

결국 호출 시점에 한번 초기화를 진행하고, 그 이후에는 가져다가 쓰기만 하는 것을 확인할 수 있다.

lazy 패턴 확인

lazy는 아래와 같이 기본 SynchronizedLazyImpl을 호출하고 있다.

public fun <T> lazy(initializer: () -> T): Lazy<T> =
	SynchronizedLazyImpl(initializer)

해당 SynchronizedLazyImpl 클래스를 확인하면 아래와 같은데 늦은 초기화를 위한 isInitialized() method와 초기화 블록인 value을 확인할 수 있다. 최초 UNINITIALIZED_VALUE으로 초기화하고, get을 통해 value 호출이 일어나면, 그때 한번 초기화하고, 이후 이미 생성한 값을 통해 불러옴을 확인할 수 있다. 거기에 자동으로 Thread safe 할 수 있도록 synchronized을 호출한다.

@JvmVersion
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                }
                else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)
}

lazy 패턴을 사용하는 것으로도 충분히 스레드 세이프 하게 동작함을 확인할 수 있다.

lazy 초기화

위에서는 단순하게 lazy 접근법을 확인하였는데 이번엔 Synchronized가 아닌 경우 별도 옵션 정의를 할 수 있다.

기본 Mode는 아래와 같이 3가지이며, default로 SYNCHRONIZED을 정의하고있다.

public enum class LazyThreadSafetyMode {

    /**
     * Locks are used to ensure that only a single thread can initialize the [Lazy] instance.
     */
    SYNCHRONIZED,

    /**
     * Initializer function can be called several times on concurrent access to uninitialized [Lazy] instance value,
     * but only the first returned value will be used as the value of [Lazy] instance.
     */
    PUBLICATION,

    /**
     * No locks are used to synchronize an access to the [Lazy] instance value; if the instance is accessed from multiple threads, its behavior is undefined.
     *
     * This mode should not be used unless the [Lazy] instance is guaranteed never to be initialized from more than one thread.
     */
    NONE,
}

lazy 초기화시에 옵션은 아래와 같이 설정할 수 있다.

val temp: String by lazy(옵션) {
	//...
}

lazy Java 코드에서 확인하기

위의 초기화를 좀 더 자세하게 살펴보려면 아래와 같이 디컴파일을 통해 추가 확인을 할 수 있다.

아래 디컴파일 결과를 보면 getSubject() 메소드를 이용한 접근을 확인할 수 있다.

getSubject에서는 KProperty로 이미 만들어진 배열의 0번째 아이템을 그냥 가지고 와서 쓰기만 한다.

이를 통해 한번 초기화 한 변수를 그냥 불러와서 계속 사용하는 것을 확인할 수 있다.

public final class ExampleUnitTest {
	static final KProperty[] $$delegatedProperties =
		new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(ExampleUnitTest.class), "subject", "getSubject()Ljava/lang/String;"))};
   private final Lazy subject$delegate;

   private final String getSubject() {
      Lazy var1 = this.subject$delegate;
      KProperty var3 = $$delegatedProperties[0];
      return (String)var1.getValue();
   }

   @Test
   public final void test() {
      String var1 = "Not Initialized";
      System.out.println(var1);
      var1 = "subject one : " + this.getSubject();
      System.out.println(var1);
      var1 = "subject two : " + this.getSubject();
      System.out.println(var1);
      var1 = "subject three : " + this.getSubject();
      System.out.println(var1);
   }

	 public ExampleUnitTest() {
      this.subject$delegate = LazyKt.lazy((Function0)null.INSTANCE);
   }
}


마무리

late-initialized-properties와 lazy delegated-properties에 대해서 정리하였다.

굳이 Nullable이 필요치 않은 곳에서는 lazy 패턴을 걸어서 주로 사용하고 있다. lateinit의 경우도 Nullable 일 필요 없고, 언제든 값을 교체하는 게 필요한 경우 사용하고 있다.

하지만 Nullable이 꼭 필요한 경우에는 사용하지 않아야 하니 주의해서 사용하길 바란다.

lateinit과 Higher-Order Function을 활용하여 interface OnClickListener 대신 아래와 같은 onClick을 만들어서 아래와 같이 사용한다.

lateinit var onClick: (position: Int) -> Unit

// 사용하는 쪽
onClick(position)

// onClick을 받는 쪽
onClick = { position ->
	println("onClick position: $position")
}


About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments