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

in #kotlin6 years ago (edited)

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 문제를 방지할 수 있다고 생각한다.

Sort:  

Congratulations @kingori2! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

You published more than 10 posts. Your next target is to reach 20 posts.

Click here to view your Board
If you no longer want to receive notifications, reply to this comment with the word STOP

To support your work, I also upvoted your post!

Support SteemitBoard's project! Vote for its witness and get one more award!

글 마지막에 초큼 흥분 하신듯 ^^;; (당연히 적용되어어야)

고맙습니다. 수정했습니다. 그런데 두번 적으니 파워풀해보이네요 ㅋㅋㅋ

Congratulations @kingori2! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 1 year!

Click here to view your Board

Support SteemitBoard's project! Vote for its witness and get one more award!

Hello @kingori2! This is a friendly reminder that you have 3000 Partiko Points unclaimed in your Partiko account!

Partiko is a fast and beautiful mobile app for Steem, and it’s the most popular Steem mobile app out there! Download Partiko using the link below and login using SteemConnect to claim your 3000 Partiko points! You can easily convert them into Steem token!

https://partiko.app/referral/partiko

Coin Marketplace

STEEM 0.16
TRX 0.13
JST 0.026
BTC 57419.72
ETH 2441.05
USDT 1.00
SBD 2.41