본문으로 바로가기
반응형

Json을 parsing 하여 Model에 채우거나 Model의 데이터를 Json으로 만들기 위해서는 많은 방법들이 존재합니다.

Android에서 사용할 때는 JSONObject를 이용하여 수동(??)으로 직접 한 땀 한 땀 할 수도 있고, 이를 지원해 주는 라이브러리를 이용할 수 도 있습니다.

그중에 가장 많이 또는 가장 흔하게 사용하는 라이브러리로는 Gson이 있죠.

특히나 Retrofit을 사용할 때 Gson과 연결하여 쉽게 서버와 통신하도록 코드를 작성할 수 있습니다.

하지만 Gson의 경우 kotlin에서 작성된 model 클래스에 default value를 설정한 멤버 변수가 있더라도 default 값이 적용되지 않을 수 있습니다.

또한 Gson의 경우 철저하게 nullability를 check하는 kotlin의 null safety를 지원하지 않기 때문에 NPE가 날 가능성이 있습니다.

(이에 대한 문제는 두번째 글인 https://tourspace.tistory.com/360?category=797357 에서 예제와 함께 다룹니다.)

이를 보완한 Kotlinx serialization에 대해서 알아봅니다.


이 글은 android 기준으로 설명을 진행합니다.

하기 링크를 참고 하였습니다.

github.com/Kotlin/kotlinx.serialization

이 글은 여러개의 series로 구성되었습니다.

2020/11/03 - [개발이야기/Kotlin] - [Kotlinx serialization] 기본 사용법#2

2020/11/17 - [개발이야기/Kotlin] - [Kotlinx serialization] Json 직렬화/역직렬화 - Builtin classes #3

2020/11/27 - [개발이야기/Kotlin] - [Kotlinx serialization] Json 직렬화/역직렬화 - Serializers #4

2020/12/04 - [개발이야기/Kotlin] - [Kotlinx serialization] Json 직렬화/역직렬화 -Polymorphism #5

2020/12/15 - [개발이야기/Kotlin] - [Kotlinx serialization] Json 직렬화/역직렬화 -JSON features #6

Gradle setting

먼저 library를 사용하기 위해서는 gradle에 추가해야 합니다.

app 단위 build.gradle에 아래와 같이 추가합니다.

apply plugin: "kotlinx-serialization"

...


dependencies {
...

// kotlin serialization
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" // or "kotlin-stdlib-jdk8"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1" // JVM dependency

...

}

project 단위 build.gradle에 아래와 같이 추가합니다.

buildscript {
    ext{
        kotlin_version = '1.4.10'
    }

    repositories {
      ...
    }
    dependencies {
        ...
        classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
    }
}

..

 

간단하게 사용할 준비는 완료되었습니다.

그럼 간단한 사용법을 알아보겠습니다.

Model -> Json string 생성 (encoding)

Model을 json string으로 변경하기 위해서는 Json.encodeToString()을 사용합니다.

private fun makeDeptJson() {
    val dept = Dept(1, "Marketing", "USA/Seattle")
    val deptJson = Json.encodeToString(dept)
    Log.d(TAG, deptJson)
}

@Serializable
data class Dept(val no: Int, val name: String, val location: String)

 

먼저 직렬화할 대상 객체를 생성하고 @Serializable annotation을 붙입니다.

그리고 간단하게 Json.encodeToString()을 이용하여 string 형태의 JSON을 생성할 수 있습니다.

결과

{"no":1,"name":"Marketing","location":"USA/Seattle"}

Json string -> Model 변경 (decoding)

반대의 동작을 하기 위해서는 json.decodeFromString() 을 사용합니다.

private fun makeDeptJson() {
   ...
    
    val deptFromJson = Json.decodeFromString<Dept>(deptJson)
    Log.d(TAG) { deptFromJson.toString() }
}

@Serializable
data class Dept(val no: Int, val name: String, val location: String)

 

결과

Dept(no=1, name=Marketing, location=USA/Seattle)

 

복합적인 Json 구성

가장 간단한 형태의 json의 구성 형태를 봤습니다.

이제 한 depth 추가하여 json에 array가 있는 형태를 만들어 보겠습니다.

예상처럼 @Serializable을 갖는 model를 추가적으로 사용하면 됩니다.

 

Dept class에 사원 list를 넣도록 합니다.

이때 사원 역시 object이며  json array 형태로 넣습니다.

먼저 사원 class를 생성합니다.

@Serializable
data class Employee(val no: Int, val name: String)

추가된 클래스의 list를 갖도록 dep 클래스를 아래와 같이 수정합니다.

@Serializable
 data class Dept(
   val no: Int,
   val name: String,
   val location: String,
   val employees: List<Employee>)

 

이제 emplyoee 정보를 만들어 json으로 만들었다가 이를 다시 parsing 해서 model에 넣는 코드를 작성합니다.

private fun makeDeptJson() {
    val employees = listOf(
        Employee(0, "Smith"),
        Employee(1, "Mike"),
        Employee(2, "John"))

    val dept = Dept(1, "Marketing", "USA/Seattle", employees)
    val deptJson = Json.encodeToString(dept)
    Log.d(TAG) { deptJson }

    val deptFromJson = Json.decodeFromString<Dept>(deptJson)
    Log.d(TAG) { deptFromJson.toString() }
}

결과는 아래와 같습니다.

{"no":1,"name":"Marketing","location":"USA/Seattle","employees":[{"no":0,"name":"Smith"},{"no":1,"name":"Mike"},{"no":2,"name":"John"}]}


Dept(no=1, name=Marketing, location=USA/Seattle, employees=[Employee(no=0, name=Smith), Employee(no=1, name=Mike), Employee(no=2, name=John)])

 

위에서 언급한 기본적인 기능 이외에도 다양한 option을 두고 json을 처리할 수 있습니다.

그중에 몇 가지만 예를 들어 보겠습니다.

Pretty print

위에서 출력한 json string값은 일기가 쉽지 않습니다. 따라서 indent와 줄 바꿈이 있는 형태로 바꿔 디버깅하기 좋도록 만들어 봅니다.

private fun makeDeptJson() {
    val prettyJson = Json { prettyPrint = true }
    ...
    val deptJson = prettyJson.encodeToString(dept)
    LogError(TAG) { deptJson }

    val deptFromJson = Json.decodeFromString<Dept>(deptJson)
    LogError(TAG) { deptFromJson.toString() }
}

이번엔 encodeToString 사용 시 기본 Json 객체가 아닌 prettyPrint option을 추가하여 만든 prettyJson 객체를 사용하도록 합니다.

간단하게 prettyPrint = true 옵션만 추가했습니다.

결과

{
    "no": 1,
    "name": "Marketing",
    "location": "USA/Seattle",
    "employees": [
        {
            "no": 0,
            "name": "Smith"
        },
        {
            "no": 1,
            "name": "Mike"
        },
        {
            "no": 2,
            "name": "John"
        }
    ]
}

 

훨씬 읽기 편한 형태도 출력되었습니다.

 

Ignore Unknown keys

보통 json에 들어가는 key값은 model의 멤버 변수와 mapping 되어야 합니다.

만약 json string에는 정의되어 있으나 model에는 해당 컬럼이 없을 경우 이는 json 파싱 규칙에 의하여 에러가 발생합니다.

이를 무시하기 위해서는 ignoreUnKnownKeys 옵션을 사용합니다.

private fun makeDeptJson() {
    val json = Json { ignoreUnknownKeys = true }      

val deptJson1 = """
                {"no":"1","name":"Marketing","location":"USA/Seattle","nickName":"Wow!!!"}
                """
    val deptFromJson = json.decodeFromString<Dept>(deptJson1)
    Log.d(TAG) { deptFromJson.toString() }
}

@Serializable
data class Dept(val no: Int, val name: String, val location: String)

Dept 모델에는 없는 nickName 값을 갖는 json string이 들어왔으나, ignoreKnownKeys = true 로 설정해 놓았기 때문에 정상적으로 존재하는 값만 파싱되어 model이 생성됩니다.

 

이 외에도 ""(quote)가 string인데 없거나, integer값에 들어있는등 잘못된 ""가 사용된 경우에도 처리가 가능한 isLenient 옵션이나, 잘못된 형태의 입력값이 존재할 때 오류없이 적절한 값으로 대체시키는 coerceInputValues 옵션 등이 존재합니다.

좀 더 상세한 내용은 하기 링크에서 확인 가능합니다.

https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md

 

Kotlin/kotlinx.serialization

Kotlin multiplatform / multi-format serialization - Kotlin/kotlinx.serialization

github.com

 

Summary

이 글에서는 가장 간단한 사용법을 설명합니다.

이 부분만 알더라도 간략하게 사용이 가능하지만, JSON을 타 기기 (ex) client <-> server) 또는 다른 버전의 앱과 주고 받으면 발생할수 있는 여러 예외사항들에 대한 처리가 가능하도록 다양한 기능들을 제공합니다.

한 예로 Retrofit에서 Kotlinx.serialization을 사용하기 위한 adapter도 이미 준비되어 있습니다.

github.com/JakeWharton/retrofit2-kotlinx-serialization-converter

 

JakeWharton/retrofit2-kotlinx-serialization-converter

A Retrofit 2 Converter.Factory for Kotlin serialization. - JakeWharton/retrofit2-kotlinx-serialization-converter

github.com

사실 사용법만 보자면 Gson과 크게 다르지 않습니다.

Gson도 충분히 심플하고 사용하기 편리하지만 주 개발 언어가 Kotlin이라면 Kotlin serialization을 사용할 때 약간의 장점이 존재 합니다.

  • Kotlin에 최적화: Nullability에 대한 Kotlin의 강력한 type check를 유지 가능
  • Refelction이 아닌 serializer를 사용하여 속도면에서 이득

다만 Gson과 비교하여 단점 이라면

  • 사용하는 방법은 둘다 간편한다 (그렇다면 굳이 Kotlinx serialization을 써야 하는 이유가?)
  • Gson은 Kotlin 뿐만 아니라 java에서도 사용가능

추가 포스팅을 통하여 더 많은 기능들을 정리 할 예정이지만, 이미 Gson을 잘 쓰고 있다면 바꿀 이유가 있을지는 현재로써는 의문입니다.

하지만 jetbrain에서 공식적으로 kotlin에 녹여내는 만큼 Kotlin안에서는 보다 강력한 기능과 확장성을 제공할것으로 예상되어 미래를 내다 본다면 kotlin serialization을 적용하는것도 나쁘지 않은 선택일것 같습니다.

반응형