Gson 은 JSONObject 를 파싱해서 객체로 만들어 사용할 때, 파싱하는 번거로움을 줄여주는 유용한 라이브러리입니다.

 

Gson 은 상당히 오래전 부터 사용해오던 라이브러리입니다.

개발을 하면서 처음 Gson 이란 놈을 만났던 것은 2010년대 초반 정도였던 것으로 기억하는데,

Github 의 정보를 보니 1.0 이 릴리즈 된것이 2008년이니 최소한 10년은 넘은 라이브러리입니다.

 

github.com/google/gson

 

google/gson

A Java serialization/deserialization library to convert Java Objects into JSON and back - google/gson

github.com

 

Gson 과 관련된 글들이 많이 있지만, 간단한 코드를 보는 것이 가장 유용합니다.

아래 코드를 보면, Person 이라는 객체의 멤버 이름을 JSONObject 의 Key 값으로  치환해서 String 으로 변환해 줍니다.

거꾸로 이 String 을 JSONObject 로 변환할 수 있다면, 다시, 이전의 Person 이라는 객체로 만들어 줄 수도 있습니다.

 

fun main(args: Array<String>) {
    val p1 = Person("John", 10)
    val json: String = GsonBuilder().create().toJson(p1)
    println(json)

    val p2: Person = GsonBuilder().create().fromJson<Person>(json, Person::class.java)
    println("name=${p2.name}, age=${p2.age}")
}

class Person(val name: String, val age: Int)

//----- Result -----
{"name":"John","age":10}
name=John, age=10

 

어떻게 이렇게 만들 수 있을까? 가 궁금해 집니다.

라이브러리 내부의 코드들은 더 복잡하고 많은 내용들을 담고 있지만, 간단하게 그 원리를 파악해보기 위해, 아래와 같은 코드를 실행해 보면, 어느 정도 짐작해 볼 수 있습니다.

코드에서는 클래스 내부에 선언된 필드들의 이름과 타입을 파악할 수 있는데, 이를 이용하면, 라이브러리를 사용하는 사람이 어떤 종류의 객체, 클래스를 생성하건 이를 이용해서 JSONObject 로 변환해 줄 수 있는 코드를 만들 수 있습니다.

- 물론, 아래와 같이 간단한 객체의 경우는 저 정도의 코드 만으로도 변환이 가능하겠지만, 실제 라이브러리의 코드를 참조하면, 더 많은 내용들이 담겨져 있고, 한 편으로는 더 많은 공부가 필요 하기도 합니다.

 

fun main(args: Array<String>) {
    val c : Class<*> = Person::class.java
    for(d in c.declaredFields){
        println("Field Name = ${d.name}, Type = ${d.type}")
    }
}

class Person(val name: String, val age: Int)

// ---- Result ----
Field Name = name, Type = class java.lang.String
Field Name = age, Type = int

 

이 글을 쓰게 된 것은 제목에서도 보이지만, 단순히 Gson 에 대해 소개하거나 설명하기 위한 글이 아닙니다.

Gson 이 Android 와 만났을 때, 그리고, Android 에서 난독화되었을 때의 문제를 풀어내기 위해서 입니다.

Android 의 proguard 는 클래스 필드명을 모조리 바꾸어 버립니다.

이를 테면, 위의 코드에서 사용했던 Person 같은 객체의 이름과 필드의 이름들은 아래와 같이 변경됩니다.

 

class A(val a: String, val b: Int)
// class Person(val name: String, val age: Int)

 

즉, 이런 식으로 바뀌면, 처음 {"name":"John","age":10} 으로 출력되었던 결과물은 {"a":"John","b":10} 으로 출력될 것입니다.

거꾸로 JSONOjbect 스트링을 객체로 변환하는 것은 당연히 실패합니다.

여기서 약간 난해한 상황은 기본 값을 갖는 객체가 생성되지 Error를 발생시킨다던가 하지 않습니다.

그래서, build variant 를 debug 로 실행할 때는 문제가 없었지만, release 로 실행할 때는 전혀 예상할 수 없는 결과가 나오게 되면 매우 당황하게 됩니다.

 

물론, github/gson android 예제로 부터 간단히 답을 찾을 수 있습니다.

(github.com/google/gson/blob/master/examples/android-proguard-example/proguard.cfg) 파일을 참조하면, 다음과 같습니다.

 

##---------------Begin: proguard configuration for Gson  ----------
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature

# For using GSON @Expose annotation
-keepattributes *Annotation*

# Gson specific classes
-dontwarn sun.misc.**
#-keep class com.google.gson.stream.** { *; }

# 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>;
}

##---------------End: proguard configuration for Gson  ----------

 

Android Library Project 로 consumer-proguard 를 추가했더라면, 상황이 달라질 수 있겠지만, Gson jar 파일로만 배포합니다.

어쨋거나, 위의 예제 프로젝트를 참고하여, proguard 파일에 똑같이 추가해주어야 별문제가 없을 것 같습니다.

그럼에도 여기서 가장 중요한 것은 아래 라인입니다.

저 부분에 Person 에 해당하는 class 파일(즉, 직접 추가한 클래스)을 넣어 주어야 합니다.

그래야, 위에서 얘기한 것처럼 난독화 할 때, name -> a 로 바뀌는 문제를 방지할 수 있습니다.

 

-keep class com.google.gson.examples.android.model.** { <fields>; }
# -keep class com.example.Person { <fields>; }