이동식 저장소

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] 디버깅할 때 앱이 느리다면  (1) 2022.09.25
[Hilt] Entry Points  (0) 2022.09.10
[Hilt] Modules  (0) 2022.08.29
Comments