이동식 저장소

Gson에서 null이 반환될 때 with ProGuard 본문

Primary/Android

Gson에서 null이 반환될 때 with ProGuard

해스끼 2022. 9. 27. 11:48

알 수 없는 에러

Proguard를 적용한 후 이상한 에러가 발생한다.

java.lang.NullPointerException: Parameter specified as non-null is null: 
method com.practice.hanbitlunch.screen.Menu.<init>, parameter name
    at com.practice.hanbitlunch.screen.Menu.<init>(Unknown Source:2)
    at com.practice.hanbitlunch.screen.MainUiStateKt.a(Unknown Source:52)
    at com.practice.hanbitlunch.screen.MainScreenViewModel.e(Unknown Source:89)
    at j4.b$a.j(Unknown Source:24)
    at j4.b$a.K(Unknown Source:12)
    at u5.o.j(Unknown Source:43)
    at u5.o.I(Unknown Source:17)
    at v5.j$a$a$a.j(Unknown Source:34)
    at v5.j$a$a$a.K(Unknown Source:12)
    at r5.a.x0(Unknown Source:25)
    at a0.c.Z(Unknown Source:46)
    at a0.c.a0(Unknown Source:11)
    ... (하략)

Non-null인 ``Menu`` 객체의 파라미터로 null이 주어진다는 뜻이다. 무슨 말인지 이해하기 위해 ``Menu``의 정의를 보자.

data class Menu(
    val name: String,
)

그냥 메뉴의 이름을 담는 단순한 pojo 객체이다. 뭐가 null이라는 거지?

흠..

1시간 정도 고민한 결과 잘 모르겠다. 그래서 데이터가 전달되는 과정을 추적하기로 했다. ``Room``에 저장된 식단 정보는 ``DataSource``, ``Repository``, ``UseCase``, ``ViewModel``을 거쳐 UI에 전달되는데, 어딘가에서 null로 바뀌었을 가능성이 높다.

 

일단 ``Room``에는 제대로 저장되어 있다. App Inspection에서 확인할 수 있다.

그렇다면 전달 과정에서 에러가 났다는 뜻이다. 이제 어느 객체에서 오류가 발생했는지 확인해 보자. 

추적 60분

로그를 찍어 본 결과, ``UseCase``에서 Room entity를 domain의 객체로 변환할 때 에러가 발생했다.

새 Logcat 최고다

``UseCase``는 변환 과정에서 ``Gson``을 사용하여 json 문자열을 Kotlin 객체로 변환한다. 다른 앱에서도 ``Gson``이 종종 문제를 일으키곤 했었는데, 사실 이건 ``Gson``의 문제라기보단 data parser의 본질적인 속성 때문이다.

 

``Gson``은 변수의 이름을 참조하여 객체를 만드는데, R8 컴파일러가 변수의 이름을 바꾸거나 아예 지워버리면 ``Gson`` 입장에서는 답이 없다. ``Menu`` 클래스의 ``name`` 속성이 ``a``로 바뀌었다고 해 보자. Json에는 당연히 ``a``라는 이름의 변수가 존재하지 않는다. 값이 없으니 ``Gson``은 항상 하던 대로 null을 넣어 반환하고, 그렇게 모든 비극이 시작되는 것이다.

해결방법

구글도 이 문제를 알고 있어서, GitHub 리포지토리에 이렇게 언급하고 있다.

 

ProGuard is a tool for 'shrinking' and obfuscating... (중략) or remove them if they appear to be unused. This can cause issues for Gson which uses Java reflection to access the fields of a class. It is necessary to configure ProGuard to make sure that Gson works correctly.

 

Gson과 ProGuard를 함께 사용하면 문제가 발생할 수 있으니, ProGuard가 Gson을 방해하지 않도록 구성할 필요가 있다. App level의 ``proguard-rules.pro`` 파일에 다음의 코드를 추가하자.

# Application classes that will be serialized/deserialized over Gson
-keep class com.google.gson.examples.android.model.** { <fields>; }

# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * extends com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer

# Prevent R8 from leaving Data object members always null
-keepclassmembers,allowobfuscation class * {
  @com.google.gson.annotations.SerializedName <fields>;
}

# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken

맨 위의 ``-keep class`` 뒤에 Gson으로 변환되는 클래스의 이름을 적어야 한다. 에러 난 것처럼 빨간 줄이 그어져도 상관 없다.

 

이제 앱을 다시 빌드해 보자. 객체가 제대로 변환된다!

후기

휴.. 이제 편히 잘 수 있겠다.

'Primary > Android' 카테고리의 다른 글

[Android] APK와 AAB  (0) 2022.10.13
[Android] Proguard가 사람 잡는다  (0) 2022.10.01
[Android] 디버깅할 때 앱이 느리다면  (0) 2022.09.25
[Hilt] Entry Points  (0) 2022.09.10
[Hilt] Modules  (0) 2022.08.29
Comments