23.12.18 lateinit/lazy 살펴보기

KSang·2023년 12월 18일
0

TIL

목록 보기
14/101

https://thdev.tech/kotlin/2018/03/25/Kotlin-lateinit-lazy/
class 생성과 동시에 변수가 초기화되면, 재 접근시 빠르게 접근이 가능하여 이득을 볼 수 있다.

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

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

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을 통해 명시가 필요한 경우

늦은 초기화를 제공하는 프로퍼티

  • Late-Initialized Properties : 늦은 초기화를 위한 Properties
  • lazy : 늦은 초기화가 가능한 Delegated Properties

Late-Initialized properties

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()
		}
	}
}

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 {}에 생성과 동시에 값을 초기화하는 방법을 사용한다.

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

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

초기화확인

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 초기화

Synchronized가 아닌 경우 별도 옵션 정의를 할 수 있다.

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

public enum class LazyThreadSafetyMode {

    /**  
     * 잠금은 단일 스레드만 [lazy] 인스턴스를 초기화할 수 있도록 하는 데 사용
     * 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(옵션) {
	//...
}

마무리

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")
}

lazy: 이는 지연 초기화(lazy initialization) 패턴을 구현하는 데 사용되며, 프로퍼티의 초기화를 해당 프로퍼티가 처음으로 접근될 때까지 지연시킨다. 이 방식은 프로퍼티의 초기화 작업이 많은 시간이나 리소스를 소모하는 경우, 또는 프로퍼티가 실제로 사용되지 않을 수도 있는 경우에 효율적이다.

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main() {
    println(lazyValue) // 첫 접근 시 "computed!"를 출력 후, "Hello" 반환
    println(lazyValue) // 두번째 접근부터는 이미 초기화가 되어있으므로 "Hello"만 반환
}

init: 이는 초기화 블록(initializer block)을 나타내며, 객체의 인스턴스가 생성될 때 실행, 이 블록 내에서 프로퍼티를 초기화할 수 있다.
init 블록은 객체가 생성되는 즉시 실행되므로, lazy와 달리 프로퍼티의 초기화를 지연시키지 않습니다.

class InitExample {
    val initValue: String

    init {
        println("init block!")
        initValue = "Hello"
    }
}

fun main() {
    val example = InitExample() // 객체 생성 시 "init block!"를 출력 후, initValue를 "Hello"로 초기화
    println(example.initValue) // "Hello" 반환
}

즉, lazy는 프로퍼티가 실제로 필요할 때까지 초기화를 지연시키며, init는 객체가 생성될 때 바로 프로퍼티를 초기화한다.

0개의 댓글