/ KOTLIN

Kotlin 1.4 Online Event: kotlinx.serialization 1.0 영상 정리

Kotlin 1.4 Online Event 영상 정리 시리즈

  1. Kotlin 1.4 Language Features
  2. News From the Kotlin Standard Library
  3. kotlinx.serialization 1.0 (현재 글)

코틀린 1.4 출시와 함께 진행된 온라인 이벤트kotlinx.serialization 1.0 세션에서 소개된 내용을 요약 정리해 보았습니다.

전체 세션 내용은 아래 영상에서 확인하실 수 있습니다.

이 세션에서 다루는 내용은 크게 다음과 같습니다.

  1. Introduction to kotlinx.serialization
    • kotlinx.serialization 라이브러리를 간단히 소개합니다.
  2. 1.0 라이브러리 살펴보기
    • kotlinx.serialization 라이브러리 1.0 버전을 살펴봅니다.
  3. Future plans
    • 추후 출시될 기능을 간략히 소개합니다.
  4. Setup and/or migration
    • kotlinx.serialization 라이브러리를 프로젝트에 적용하는 방법을 소개합니다.

Introduction to kotlinx.serialization

코틀린에서 JSON 파서가 필요할 때, 다음과 같은 라이브러리를 사용할 수 있습니다.

당장 사용할 수 있는 괜찮은 라이브러리가 이미 있는데, 왜 코틀린을 위한 JSON 라이브러리가 추가로 필요할까요? 이유는 간단합니다. 이들은 모두 자바 라이브러리이기 때문입니다.

코틀린은 대개 자바와 호환되지만, 그렇지 않은 경우도 있습니다. 몇 가지 예를 통해 자세히 살펴보겠습니다.

멀티플랫폼 지원

앞에서 소개한 라이브러리는 자바 라이브러리이므로, 자바를 지원하는 플랫폼 (예: 안드로이드, Spring framework 등)에서만 사용할 수 있습니다.

반면에, kotlinx.serialization 라이브러리는 자바 (JVM), 자바스크립트, 네이티브 (Kotlin/Native)와 같이 다양한 플랫폼을 지원합니다. 따라서. 직렬화 (Serialization)를 담당하는 코드를 각 플랫폼별로 따로 작성할 필요 없이 공용 라이브러리에 구현할 수 있습니다.

코틀린 지향 (Kotlin-oriented)

다음과 같이 namelanguage 프로퍼티로 구성된 Project 클래스가 있다고 가정해 봅시다. language 프로퍼티의 기본값이 "Kotlin"인 것에 주목해 주세요.

@Serializable
data class Project(
    val name: String,
    val language: String = "Kotlin"
)

이 때, 다음 코드와 같이 name 프로퍼티만 있고 language 프로퍼티는 없는 JSON 문자열을 파싱하면 어떤 일이 일어날까요?

const val inputString = 
"""{"name":"kotlinx.serialization"}"""

이 문자열을 Gson 라이브러리로 파싱하면, name에는 정상적으로 값이 대입되지만 language에는 null이 대입됩니다. 즉, null이 아닌 프로퍼티가 null 값을 가지게 될 뿐 아니라, language 프로퍼티의 기본값 조차 반영되지 않는 문제가 발생합니다.

// Project(name=kotlinx.serialization, language=null) 이 출력됩니다.
println(Gson().fromJson(inputString, Project::class.java))

반면에, kotlinx.serialization 라이브러리를 사용하면 inputString내 JSON 문자열이 language 프로퍼티를 포함하고 있지 않음을 확인하고, null 대신 기본값을 대입합니다. 따라서 개발자가 의도한 대로 파싱이 수행되는 것을 확인할 수 있습니다.

// Project(name=kotlinx.serialization, language=Kotlin)
println(Json.decodeFromString<Project>(inputString))

컴파일 안전 보장 (compile-time safe)

다른 라이브러리와 달리, kotlinx.serialization 라이브러리는 @Serializable 어노테이션이 있는 클래스만 직렬화(serialization)를 지원합니다.

다음과 같이 직렬화를 지원하는 클래스인 Project 클래스와 그렇지 않은 클래스인 User 클래스가 있다고 가정해 봅시다.

@Serializable
data class Project(
    val name: String,
    val language: String = "Kotlin"
)

// @Serializable 어노테이션이 없으므로 직렬화를 지원하지 않습니다.
data class User(val userName: String)

이 떄, 직렬화를 지원하지 않는 클래스인 User 클래스를 직렬화를 지원하는 Project 클래스의 프로퍼티로 넣으면 다음과 같이 컴파일 에러가 발생합니다.

@Serializable
data class Project(
    val name: String,
    val owner: User, // 컴파일 에러: Serializer for type User has not been found
    val language: String = "Kotlin"
)

// @Serializable 어노테이션이 없으므로 직렬화를 지원하지 않습니다.
data class User(val userName: String)

즉, 직렬화가 제대로 수행될 수 않는 경우 런타임 에러 대신 컴파일 에러가 발생하므로 버그를 미연에 방지할 수 있습니다.

명료함 (explicit) 및 간결함 (concise)

명료함과 간결함은 코틀린 언어의 대표적인 특징입니다. kotlinx.serialization 라이브러리 또한 이러한 코틀린 언어의 철학에 부합하게끔 설계되어 있습니다.

다음은 Gson 라이브러리를 사용하여 List<Project> 데이터를 담고 있는 JSON 문자열을 파싱하는 코드를 보여줍니다.

val projectList = Gson().fromJson<List<Project>>(
    inputStringList, List::class.java
)

이 때, 파싱된 결과 리스트의 타입을 확인해 보면 Project 가 아닌 LinkedTreeMap인 것을 알 수 있습니다.

// class com.google.gson.internal.LinkedTreeMap
println(projectList.first()::class.java)

이는 JVM이 제네렉 인자의 타입 정보를 소거 (type erasure) 하기 때문이며, 이러한 이유로 GSON 라이브러리는 Project 라는 타입 대신 어떠한 형태의 데이터도 받아들일 수 있는 LinkedTreeMap 타입으로 객채를 생성합니다.

물론, 다음과 같이 TypeToken를 넘겨주면 GSON 라이브러리를 사용하더라도 Project타입으로 객체를 생성하도록 구성할 수 있습니다.

val projectList = Gson().fromJson<List<Project>>(
    inputStringList,
    (object: TypeToken<List<Project>>() {}).type // 타입 정보를 넘겨줍니다.
)

// class kotlinx.serialization.formats.json.Project
println(projectList.first()::class.java)

의도한 대로 동작하기는 하지만, 위 코드에서 볼 수 있듯이 코드가 꽤나 성가신(?!) 모습을 하고 있습니다.

게다가, 타입 토큰이 왜 들어가게 되었는지 배경을 잘 모른다면 이해가 어렵기에 여러모로 접근성이 좋지 않은 코드라 볼 수 있습니다.

반면에, kotlinx.serialization 라이브러리는 사용하면 GSON 라이브러리와 달리 직관적인 코드만으로 List<Project> 타입으로 정상적으로 파싱할 수 있습니다.

val projectList = 
    JSON.decodeFromString<List<Project>>(inputStringList)

// class kotlinx.serialization.formats.json.Project
println(projectList.first()::class.java)

이처럼 간단하게 파싱을 할 수 있는 이유는, 코틀린 1.4에 새로 추가된 typeOf()함수 덕분입니다.

함수 원형은 다음과 같습니다.

public inline fun <reified T> typeOf(): KType

KType은 타입 토큰과 거의 동일하게 동작하며, 따라서 제네릭 인자 정보를 획득 (capture)할 수 있습니다. 예를 들면, 다음과 같이 복잡한 제네릭 객체 정보도 잘 표현하는 것을 확인할 수 있습니다.

val type = typeOf<Box<List<StringData>>>()

// "kotlinx.serializabion.Box<
//   kotlin.collections.List<
//    kotlinx.serialization.StringData>>""
println(type)

함수 명명 규칙

kotlinx.serialization 라이브러리는 다양한 포맷을 지원합니다. 현재 지원하는 포맷은 다음과 같습니다.

포맷과 무관하게, 직렬화/역직렬화에 사용하는 함수 이름은 정해진 명명 규칙을 따르고 있습니다. 직렬화(serialization)를 수행하는 함수는 encodeToXXX로, 역 직렬화(deserialization)를 수행하는 함수는 decodeFromXXX 형식을 따릅니다.

다음은 몇 가지 예를 보여줍니다.

  • encodeToByteArray: ByteArray 형식으로 직렬화 수행 (코틀린 클래스 -> ByteArray)
  • decodeFromByteArray: ByteArray 형식으로부터 역 직렬화 수행 (ByteArray -> 코틀린 클래스)
  • encodeToString: String 형식으로 직렬화 수행 (코틀린 클래스 -> String)
  • decodeFromString: String 형식으로부터 역 직렬화 수행 (String -> 코틀린 클래스)

1.0 라이브러리 삺펴보기

API

다음과 같은 기본 기능들은 안정 버전 API로 제공됩니다.

  • 클래스 <-> String 간 직렬화/역직렬화
  • @Serializable 어노테이션을 포함한 직렬화 관련 어노테이션
  • 다형성 (polymorphic) 직렬화
  • JSON tree API (커스텀 직렬화 포함)
  • 간단한 Custom serializer 지원

하지만, 다음과 같은 고급 기능들은 아직 실험 버전 API로만 지원됩니다.

  • Custom 직렬화 포맷 (사용자가 원하는 형태로 데이터 인코딩)
  • 스키마 확인 (Schema introspection)
  • CBOR, Protobuf, HOCON, and Properties 포맷 지원

이 외에, @ExperimentalSerializationApi로 표시된 모든 API는 실험 버전 API로 추후 기능이 예고없이 변경될 수 있습니다.

새 기능 소개

1.0 버전에서 추가된 새로운 기능들은 프로젝트 저장소 내 README에서 자세히 확인할 수 있으며, 여기에서는 대표적인 기능 두 가지를 집중적으로 살펴봅니다.

조금 더 유연한 역 직렬화 지원 (More-flexible deserialization)

kotlinx.serialization 라이브러리는 코틀린 클래스에 정의된 타입을 우선시합니다. 따라서, JSON 문자열 내 포함된 객체가 역 직렬화 (deserialization) 대상 클래스 내 프로퍼티 정의에 부합하지 않으면 예외를 발생시킵니다.

다음과 같이 모든 프로퍼티가 널 값을 가질 수 없는 클래스인 Project가 있다고 가정해 봅시다.

@Serializable
data class Project(
    val name: String
    val language: String = "Kotlin"
)

이 때, 다음과 같이 language 값이 null인 JSON 문자열을 파싱하려 한다면 런타임 오류가 발생합니다.

// 오류: language 프로퍼티에 null 값을 대입할 수 없습니다.
println(Json.decodeFromString<Project>(
    """{"name:"Ktor","language":null}"""
))

이 때, coerceInputValues 플래그를 활성화 하면 런타임 오류를 발생시키는 대신 기본값인 "Kotlin" 문자열을 대입하게끔 설정할 수 있습니다.

// 값을 대입할 수 없을 때 기본값을 대입하도록 설정합니다.
val json = Json { coerceInputValues = true }

// Project(name=Ktor, language=Kotlin)
println(json.decodeFromString<Project>(
    """{"name:"Ktor","language":null}"""
))

다만, 모든 예외 상황을 커버하지는 않고, 현재는 다음 두 가지 상황에서만 기본값을 사용하게끔 처리해줍니다.

  • null이 아닌 타입에 null이 대입되는 경우
  • enum 타입에 알려지지 않은 값이 대입되는 경우

이와 관련된 조금 더 자세한 내용은 개발자 문서를 참고하세요.

역 직렬화시 다형성 지원 (Polymorphic deserialization)

REST API의 응답으로 내려오는 JSON 객체를 보면, 응답에 대한 공통 필드 (예: 응답 코드 및 메시지)와 함께 응답 유형별로 다른 필드를 추가로 제공하는 경우가 많습니다.

다음 두 예시를 보면 type, name, language 필드는 공통으로 가지고 있지만 마지막 필드만 다른 것을 확인할 수 있습니다.

{
    "name" : "Kotlin",
    "language" : "Kotlin",
    "owner" : "JetBrains"
}
{
    "name" : "Kotlin",
    "language" : "Kotlin",
    "stars" : 1
}

이러한 구조는 코틀린 코드에서 다음과 같이 구성할 수 있습니다. Project 클래스가 sealed 클래스로 선언된 것에 주목해 주세요.

@Serializable
sealed class Project {
    abstract val name: String
    abstract val language: String
}

@Serializable
class OwnedProject(
    override val name: String,
    override val language: String,
    val owner: String,
): Project()

@Serializable
class StarredProject(
    override val name: String,
    override val language: String,
    val stars: Int,
): Project()

베이스 클래스인 Project 클래스를 sealed 클래스로 선언해야 kotlinx.serialization 라이브러리에서 파생 클래스 (OwnedProject, StarredProject)에 대응하는 Serializer를 생성해 줍니다. (참조: 개발자 문서)

따라서, 다음과 같이 Project 타입을 인자로 넘겨주어도 데이터의 입력 형태에 따라 파생 타입으로 올바르게 객체를 만들어 줍니다.

Json.decodeFromString<Project>(
 """{"name":"Kotlin","language":"Kotlin","owner":"JetBrains"}"""
)
=> OwnedProject(name=Kotlin, language=Kotlin, owner=JetBrains)

Json.decodeFromString<Project>(
 """{"name":"kotlinx.serialization","language":"Kotlin","stars":2200}"""
)
=> StarredProject(name=kotlinx.serialization, language=Kotlin, stars=2200)

만약, 위에서 정의된 파생 클래스가 아닌 알려지지 않은 형태의 입력이 들어오는 경우, 이를 어떤 타입으로 변환해야 할지 모르기에 런타임 에러가 발생합니다.

Json.decodeFromString<Project>(
 """{"name":"Kotlin","language":"Kotlin","forks":4100}"""
)
=> Polymorphic serializer was not found for class discriminator 'Forked'

이 때, 런타임 에러를 발생시키는 필수 필드 (여기에서는 namelanguage) 만으로 구성된 객체로라도 변환하길 원하는 경우, 필드로 구성된 클래스를 정의한 후 SerializerModule을 설정해 주면 됩니다.

class BaseProject(
    override val name: String,
    override val language: String,
): Project()

val module = SerializersModule {
    polymorphic(Project::class) {
        default { 
            // 적합한 타입을 찾지 못한 경우 BaseProject 타입으로 변환합니다.
            // BaseProject.serializer() 함수는 kotlinx.serialization 라이브러리에서 자동으로 생성해줍니ㅏㄷ.
            BaseProject.serializer()
         }
    }
}

향후 계획 (Future plans)

  • java.io.* 스트림을 지원하는 API가 1.1 버전에서 추가될 예정입니다. (JVM 전용)
  • kotlinx-io 라이브러리 (멀티플랫폼을 위한 I/O 라이브러리)와의 통합도 지원할 예정입니다.
  • 인라인 클래스 지원 또한 추가될 예정입니다.

라이브러리 설정 방법

kotlinx.serialization 라이브러리를 프로젝트에서 사용하려면 아래 두 가지 방법 중 하나를 따르면 됩니다.

plugins 블록 사용하기

프로젝트 레벨 빌드스크립트에 다음과 같이 플러그인을 선언합니다.

[Kotlin DSL]

plugins {
    ...
    kotlin("plugin.serialization") version "1.4.10"
}

[Groovy DSL]

plugins {
    ...
    id 'org.jetbrains.kotlin.plugin.serialization' version '1.4.10'
}

apply plugin 사용하기

프로젝트 루트 빌드스크립트에 serialization 플러그인을 classpath로 추가합니다.

[Kotlin DSL]

buildscript {
    repositories { jcenter() }

    dependencies {
        ...
        classpath(kotlin("serialization", version = kotlinVersion))
    }
}

[Groovy DSL]

buildscript {
    ext.kotlin_version = '1.4.10'
    repositories { jcenter() }

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

다음, 애플리케이션 레벨 빌드크스크립트에 플러그인을 적용해줍니다.

apply plugin: 'kotlinx-serialization'

마지막으로, 사용할 포맷에 맞는 라이브러리를 의존성에 추가합니다. 다음은 JSON용 라이브러리를 추가하는 예를 보여줍니다.

[Kotlin DSL]

repositories {
    // Artifacts are also available on Maven Central
    jcenter()
}

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
}

[Groovy DSL]

repositories {
    // Artifacts are also available on Maven Central
    jcenter()
}

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
}

추가 리소스

kunny

커니

안드로이드와 오픈소스, 코틀린(Kotlin)에 관심이 많습니다. 한국 GDG 안드로이드 운영자 및 GDE 안드로이드로 활동했으며, 현재 구글에서 애드몹 기술 지원을 담당하고 있습니다.

Read More