Kotlin의 lazy field를 가진 serialize 객체를 Proguard 적용할 때 주의할 점

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) 

원인을 분석한 결과는 다음과 같다.

  1. lazy property는 java 바이트코드로 만들어질 때 클래스 내부에 lazy 타입의 필드를 선언한다.
    public final class User implements Serializable {
      @NotNull
      private final Lazy nameType$delegate;
    }
    
  2. 실행 후 디버거를 찍어보면 저 필드에 할당되는 구현체는 kotlin stdlib에 들어있는 SynchronizedLazyImpl 클래스이다.
    private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    
      ...
      private fun writeReplace(): Any = InitializedLazyImpl(value)
    }
    
    위 코드에서 보듯, SynchronizedLazyImplSerializable 인터페이스를 구현하며, writeReplace() 펑션을 이용해 자신이 serialize될 때 까지 아직 평가되지 않은 상태라면 평가를 진행하고, 그 값을 serialize 한다. 아직 평가되지 않았을 때 가지는 값은 UNINITIALIZED_VALUE 라는 객체이다.
  3. (상상) Proguard를 거치기 전엔 위의 구현이 문제없이 동작한다. 하지만 Proguard 난독화/최적화를 거치면서 저 writeReplace() 메서드가 사라지는 것 같다. 그래서 Proguard를 적용한 후엔 lazy 펑션 내부의 람다를 평가해 얻은 값이 아닌, UNINITIALIZED_VALUE 자체를 직렬화하려다 실패한다.

이 문제를 해결하는 방법은 간단하다. writeReplace() 펑션이 지워지지 않도록 다음의 keep rule을 프로가드 설정 파일에 추가하면 된다.

-keepclassmembers class * {
 *** writeReplace();
}

다음에 하게 되는 고민은 저 keep 규칙이 위험할까? 막 써도 될까? 인데, 내 생각엔 저 keep 규칙은 당연히 적용되어야 한다고 생각한다. 안그러면 온갖 custom serialize 규칙이 proguard를 거치면서 다 망가지지 않을까? 그래서 kotlin 프로젝트에 proguard를 적용한다면, 저 rule은 꼭 추가해두어야 혹시나 어디선가 발생할 serialize 문제를 방지할 수 있다고 생각한다.

H2
H3
H4
3 columns
2 columns
1 column
Join the conversation now
Logo
Center