최근 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 등)를 활용할 수도 있습니다.