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 : 늦은 초기화를 위한 Properties
- lazy : 늦은 초기화가 가능한 Delegated Properties
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()
}
}
}
isInitialized
는 Higher-Order function
과 kotlin 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")
}
Comments