본문 바로가기

안드로이드/Util

[안드로이드] Dynamic Json Deserialization


최근 API의 Response 형태를 살펴보면 json이 다양한 형태로 응답을 주는 경우도 있습니다. 안드로이드에서 이러한 Dynamic JSON을 효과적으로 다루는 방법에 대해 포스팅하겠습니다.

// 다양한 형태로 오는 json
[
    {
    	"type": "USER",
        "name": "김철수",
        "age": 20,
        "birthday": "20001201"
    },
    {
    	"type": "GUEST",
        "nickname": "닉네임"
    },
	...
]


먼저 Data Class 정의 방법부터 살펴보겠습니다.

1. Dynamic Json을 위한 Data Class 정의

1-1. 모든 필드에 대해 nullable을 지정하는 방법 (Bad Case)

dynamic json 데이터를 처리할 때, 간단하게 생각하면 아래와 같이 모든 필드를 추가하고 nullable로 지정할 수 있습니다.

// bad case
data class PersonDto(
    val type: String,
    val name: String?,
    val age: Int?,
    val birthday: String?,
    val nickname: String?
)


하지만 응답의 형태가 다양해질수록 data class의 역할은 모호해지고 복잡해질 수 있습니다.

어떻게 하면 다형성을 유지하면서 dynamic json을 처리할 수 있을까요?

1-2. Gson의 JsonDeserializer을 사용하는 방법 (Good Case)

Gson의 JsonDeserializer를 이용하면 type을 판단해서 적절한 class로 변환하도록 구현할 수 있습니다.

먼저, sealed class를 이용해서 각각의 응답 형태에 대한 data class를 정의합니다.
알지 못하는 type에 대한 예외처리를 하기 위해서 Unknown이라는 서브클래스도 정의했습니다.

sealed class PersonDto {
    data class User(
        val type: String,
        val name: String,
        val age: Int,
        val birthday: String
    ): PersonDto()

    data class Guest(
        val type: String,
        val nickname: String
    ): PersonDto()
    
    data object Unknown: PersonDto()
}


그 후, type을 검사하여 적절한 서브 클래스로 변환시켜 주는 Gson의 JsonDeserializer을 구현합니다.

object PersonDtoDeserializer : JsonDeserializer<PersonDto> {

    private const val TYPE_USER = "User"
    private const val TYPE_GUEST = "Guest"
    
    override fun deserialize(
        json: JsonElement,
        typeOfT: Type,
        context: JsonDeserializationContext
    ): PersonDto = try {
    
        val type = json.asJsonObject.get("type").asString

        // 각 cell_type에 따라 적절한 서브클래스를 직접 반환
        when (type) {
            TYPE_USER -> {
                context.deserialize(
                    json,
                    PersonDto.User::class.java
                )
            }

            TYPE_GUEST -> {
                context.deserialize(
                    json,
                    PersonDto.Guest::class.java
                )
            }

            else -> context.deserialize(json, PersonDto.Unknown::class.java)
        }
    } catch (_: Exception) {
        context.deserialize(json, PersonDto.Unknown::class.java)
    }
}


마지막으로 JsonAdapter 어노테이션을 사용해서 PersonDto와 PersonDtoDeserializer를 연결해 주면
데이터를 가져올 때 type에 따라 적절한 객체를 deserialize 해서 가져오게 됩니다.

@JsonAdapter(PersonDtoDeserializer::class)
sealed class PersonDto {
    data class User(
        val type: String,
        val name: String,
        val age: Int,
        val birthday: String
    ): PersonDto()

    data class Guest(
        val type: String,
        val nickname: String
    ): PersonDto()
    
    data object Unknown: PersonDto()
}

2. Retrofit을 통한 네트워크 통신과의 호환

Retrofit을 사용할 때도 연동하는 방법은 무척 간단합니다.

Retrofit의 객체에 GsonConverterFactory를 추가하여 별다른 작업 없이 Dynamic Json을 처리할 수 있습니다.

위에서 추가한 JsonAdapter 어노테이션 PersonDtoDeserializer 덕분에 type에 맞춰 자동으로 deserialize가 수행되기 때문입니다.

fun provideRetrofit(client: OkHttpClient): Retrofit {
    return Retrofit.Builder()
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .addConverterFactory(GsonConverterFactory.create()) // GsonConverterFactory
        .client(client)
        .baseUrl(BuildConfig.BASE_URL)
        .build()
}

// RetrofitApi
interface RetrofitApi {
    @GET("endpoint")
    suspend fun getPersonList(): PersonResponse
}

// PersonResponse.kt
data class PersonResponse(
    val personList: List<PersonDto>
)
// 사용방법
retrofitApi.getPersonList().personList.forEach { personDto ->
    when (personDto) {
        is PersonDto.User -> {
            // User 타입
        }

        is PersonDto.Guest -> {
            // Guest 타입
        }

        else -> {
            // 정의되지 않은 타입   
        }
    }
}

3. 정리

이번 포스팅에서는 Gson의 JsonDeserialzer을 사용해 Dynamic Json을 처리하는 방법을 살펴보았습니다.

다양한 형태의 JSON 응답을 효율적으로 처리하기 위해 다형성을 유지하고, 필요시에는 라이브러리(Moshi 등)를 활용할 수도 있습니다.