Kotlin의 Delegated proproperties는 매우 유용한 기능이다. 언어가 제공하는 lazy 펑션을 이용하면필드를 lazy하게 초기화 할 수 있다.
다음과 같은 객체를 생각해보자.
class User: Serializable {
val _name = Name("wilson", Name.NameType.A)
val nameType by lazy {
_name.nameType
}
}
class Name(val text:String, val nameType:NameType): Serializable {
enum class NameType {
A,B
}
}
User
객체는 Serializeable
인터페이스를 구현하므로 직렬화 할 수 있다. nameType 이란 프로퍼티는 lazy
펑션을 이용하였으므로 최초 접근 시 _name
필드의 NameType
값이 할당된다.
안드로이드에서 활용해보면 어떨까? 다음 gist 는 안드로이드 기본 애플리케이션을 살짝 고쳐 본 코드이다. 버튼을 누르면 intent의 extra 에 User
객체를 저장하여 액티비티를 실행한다. 아무 문제 없어보인다. 아무 문제가 없다.
하지만 Proguard를 이용해 난독화/최적화를 수행해보면 다음과 같은 예외를 뿜으며 크래시난다.
Caused by: java.io.NotSerializableException: a.bn
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1240)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1604)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1565)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1488)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1234)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1604)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1565)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1488)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1234)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:354)
at android.os.Parcel.writeSerializable(Parcel.java:1701)
at android.os.Parcel.writeValue(Parcel.java:1654)
at android.os.Parcel.writeArrayMapInternal(Parcel.java:867)
at android.os.BaseBundle.writeToParcelInner(BaseBundle.java:1579)
at android.os.Bundle.writeToParcel(Bundle.java:1233)
at android.os.Parcel.writeBundle(Parcel.java:907)
at android.content.Intent.writeToParcel(Intent.java:9961)
at android.app.IActivityManager$Stub$Proxy.startActivity(IActivityManager.java:3730)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1669)
at android.app.Activity.startActivityForResult(Activity.java:4586)
at android.support.v4.app.n.startActivityForResult(Unknown Source:10)
at android.app.Activity.startActivityForResult(Activity.java:4544)
at android.support.v4.app.n.startActivityForResult(Unknown Source:10)
at android.app.Activity.startActivity(Activity.java:4905)
at android.app.Activity.startActivity(Activity.java:4873)
at com.example.myapplication.MainActivity$a.onClick(Unknown Source:25)
원인을 분석한 결과는 다음과 같다.
- lazy property는 java 바이트코드로 만들어질 때 클래스 내부에 lazy 타입의 필드를 선언한다.
public final class User implements Serializable { @NotNull private final Lazy nameType$delegate; }
- 실행 후 디버거를 찍어보면 저 필드에 할당되는 구현체는 kotlin stdlib에 들어있는
SynchronizedLazyImpl
클래스이다.
위 코드에서 보듯,private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable { ... private fun writeReplace(): Any = InitializedLazyImpl(value) }
SynchronizedLazyImpl
는Serializable
인터페이스를 구현하며,writeReplace()
펑션을 이용해 자신이 serialize될 때 까지 아직 평가되지 않은 상태라면 평가를 진행하고, 그 값을 serialize 한다. 아직 평가되지 않았을 때 가지는 값은 UNINITIALIZED_VALUE 라는 객체이다. - (상상) Proguard를 거치기 전엔 위의 구현이 문제없이 동작한다. 하지만 Proguard 난독화/최적화를 거치면서 저
writeReplace()
메서드가 사라지는 것 같다. 그래서 Proguard를 적용한 후엔 lazy 펑션 내부의 람다를 평가해 얻은 값이 아닌, UNINITIALIZED_VALUE 자체를 직렬화하려다 실패한다.
이 문제를 해결하는 방법은 간단하다. writeReplace()
펑션이 지워지지 않도록 다음의 keep rule을 프로가드 설정 파일에 추가하면 된다.
-keepclassmembers class * {
*** writeReplace();
}
다음에 하게 되는 고민은 저 keep 규칙이 위험할까? 막 써도 될까? 인데, 내 생각엔 저 keep 규칙은 당연히 적용되어야 한다고 생각한다. 안그러면 온갖 custom serialize 규칙이 proguard를 거치면서 다 망가지지 않을까? 그래서 kotlin 프로젝트에 proguard를 적용한다면, 저 rule은 꼭 추가해두어야 혹시나 어디선가 발생할 serialize 문제를 방지할 수 있다고 생각한다.