kotlinx.serialization: 개요, 어노테이션 및 JSON 시리얼라이제이션

저자: IT Sectr 게시일: 2026-03-15 읽는 시간: 12 분

kotlinx.serialization — JetBrains의 멀티플랫폼 라이브러리로, 리플렉션 없이 Kotlin 객체를 JSON, ProtoBuf, CBOR 및 기타 형식으로 변환합니다. Gson과 Moshi와 달리, @Serializable 어노테이션을 통해 컴파일 시간에 시리얼라이저 코드를 생성하여 높은 성능과 타입 안전성을 제공합니다. GitHub Kotlin/kotlinx.serialization에 따르면, 이 라이브러리는 Kotlin/JVM, Kotlin/Native, Kotlin/JS 및 Kotlin/Wasm을 지원합니다.

주요 포인트

  • kotlinx.serialization — 컴파일 시간 시리얼라이제이션: 코드가 컴파일 시에 생성되며, 리플렉션이 사용되지 않습니다
  • @Serializable — 클래스에 대한 시리얼라이저 생성을 시작하는 기본 어노테이션
  • Json {} builder — Json { ignoreUnknownKeys = true; prettyPrint = true }를 통한 JSON 구성
  • 멀티플랫폼 — API 변경 없이 JVM, Native, JS 및 Wasm에서 동작
  • 커스텀 시리얼라이저 — 비표준 데이터 형식을 위한 KSerializer 인터페이스

kotlinx.serialization이란?

kotlinx.serialization은 JetBrains가 공식 Kotlin 생태계의 일부로 개발한 Kotlin 용 내장 시리얼라이제이션 라이브러리입니다. 타사 제품(Gson, Moshi, Jackson)과의 주요 차이점은 실행 시에 리플렉션을 사용하지 않는다는 것입니다. 대신 시리얼라이저 코드는 Kotlin Symbol Processing (KSP) 또는 Kotlin 컴파일러 플러그인을 사용하여 컴파일 시간에 생성됩니다. 이를 통해 Gson보다 3–5배 높은 성능 향상과 타입 안전성이 보장됩니다.

이 라이브러리는 공식으로 4가지 형식을 지원합니다: JSON(kotlinx-serialization-json 모듈 통해), ProtoBuf(kotlinx-serialization-protobuf), CBOR(kotlinx-serialization-cbor) 그리고 HOCON(kotlinx-serialization-hocon). 각 형식은 build.gradle.kts에서 별도 의존성으로 추가되므로, 프로젝트에 필요 없는 라이브러리를 가져오지 않습니다. 각 형식에는 고유한 구성 매개변수 세트가 있습니다.

멀티플랫폼은 이 라이브러리의 주요 기능입니다. @Serializable이 있는 동일한 클래스가 JVM(Android, Backend), Native(iOS), JS(Web, React) 및 Wasm(WebAssembly)같은 모든 대상에서 동작합니다. 개발자는 플랫폼마다 다른 시리얼라이제이션 구현을 작성할 필요가 없습니다 — 코드는 통일됩니다. 이는 공유 코드가 Android와 iOS에서 사용되는 Kotlin Multiplatform Mobile (KMM) 프로젝트에서 특히 가치가 있습니다.

컴파일 시간 코드 생성 작동 방식

kotlinx.serialization의 코드 생성은 세 단계로 이루어집니다. 첫 번째 단계에서 Kotlin 컴파일러가 클래스에서 @Serializable 어노테이션을 감지하고 이를 Kotlin Symbol Processing (KSP) 플러그인에 전달합니다. 둘째 단계에서 KSP가 KSerializer 인터페이스를 구현하는 시리얼라이저 객체를 생성합니다. 셸째 단계에서 생성된 코드가 프로젝트의 소스 코드와 함께 컴파일됩니다. 그 결과, 이 단계는 어느 것도 애플리케이션 실행 중에 실행되지 않습니다.

생성된 시리얼라이저는 리플렉션 없이, 겟터와 셋터를 통해 클래스의 필드와 직접 작용합니다. 이는 private 수식자가 있는 필드도 @Serializable로 표시된 경우 시리얼라이제이션된다는 것을 의미합니다. 이 접근 방식의 성능은 수동 시리얼라이제이션에 근접합니다: 간단한 클래스(5–10개 필드)의 경우 시리얼라이제이션 시간은 10–50 마이크로초; 복잡한 객체 그래프의 경우 1000개 객체당 최대 200 마이크로초입니다.

Android 또는 Kotlin/JVM 프로젝트에 라이브러리를 추가하려면 build.gradle.kts에 플러그인과 의존성을 추가해야 합니다. Kotlin 버전과 일치하는 org.jetbrains.kotlin.plugin.serialization 플러그인이 코드 생성을 활성화합니다. kotlinx-serialization-json 라이브러리는 Kotlin 버전과 무관한 버전으로 dependencies 섹션에 추가됩니다.

kotlin
// build.gradle.kts — kotlinx.serialization 추가
plugins {
    val kotlinVersion = "2.1.0"
    kotlin("jvm") version kotlinVersion
    kotlin("plugin.serialization") version kotlinVersion
}

dependencies {
    // 기본 시리얼라이제이션 모듈
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")

    // 추가 형식
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.7.3")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-cbor:1.7.3")
}

기본 사용법: JSON 시리얼라이제이션

JSON은 kotlinx.serialization에서 가장 인기있는 형식입니다. 객체를 시리얼라이즈하려면 data class에 @Serializable 어노테이션을 붙이고 Json.encodeToString()을 호출하면 됩니다. 디시리얼라이제이션을 위해서는 타입을 지정하여 Json.decodeFromString()을 호출합니다. 라이브러리는 null 필드, 리스트, 중첩 객체 및 어널문을 자동으로 처리합니다. 별도로 명시하지 않는 한 클래스의 모든 필드는 기본적으로 필수입니다.

JSON 구성은 Json {} builder를 통해 수행됩니다. 디시리얼라이제이션 시 알 수 없는 필드를 스킵하려면 ignoreUnknownKeys = true, 형식화된 출력을 위해 prettyPrint = true, 유효하지 않은 값을 기본값으로 변환하려면 coerceInputValues = true를 전달할 수 있습니다. 기본값이 있는 필드를 시리얼라이즈하는 encodeDefaults와 다형성 시리얼라이제이션을 위한 classDiscriminator도 사용 가능합니다.

kotlin
// JSON 시리얼라이제이션 및 디시리얼라이제이션 예제
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration

@Serializable
data class Project(
    val name: String,
    val stars: Int,
    val isActive: Boolean = true,
    val languages: List<String> = emptyList()
)

fun main() {
    val project = Project(
        name = "kotlinx.serialization",
        stars = 7200,
        languages = listOf("Kotlin", "Java")
    )

    // prettyPrint를 사용한 JSON 시리얼라이제이션
    val json = Json { prettyPrint = true }
    val jsonString = json.encodeToString(project)
    println(jsonString)
    /*
    {
        "name": "kotlinx.serialization",
        "stars": 7200,
        "isActive": true,
        "languages": ["Kotlin", "Java"]
    }
    */

    // JSON에서 디시리얼라이제이션
    val decoded = json.decodeFromString<Project>(jsonString)
    println(decoded.name)  // kotlinx.serialization
}

이 예제는 시리얼라이제이션과 디시리얼라이제이션의 기본 사이클을 보여줍니다. @Serializable 어노테이션이 있는 data class Project는 자동으로 encodeToString과 decodeFromString을 획득합니다. isActive 필드의 기본값은 true로, JSON에서 이 필드가 없으면 기본값이 사용됩니다. ignoreUnknownKeys = true 없이 JSON에 알 수 없는 필드가 온 경우 SerializationException이 발생합니다.

Sealed class의 다형성 시리얼라이제이션

Sealed class는 kotlinx.serialization의 가장 강력한 사용 사례 중 하나입니다. 이 라이브러리는 추가 구성 없이 sealed class 계층에 대한 다형성 시리얼라이제이션을 지원합니다: sealed class와 그의 모든 하위 클래스를 @Serializable로 어노테이트하기만 하면 됩니다. 시리얼라이제이션 시 “type” 필드가 추가되며(classDiscriminator로 구성 가능), 디시리얼라이제이션 시 구체적인 타입을 결정합니다.

kotlin
// Sealed class의 다형성 시리얼라이제이션
@Serializable
sealed class Response

@Serializable
data class Success(val data: String) : Response()

@Serializable
data class Error(val code: Int, val message: String) : Response()

fun main() {
    val json = Json { classDiscriminator = "result_type" }

    val responses: List<Response> = listOf(
        Success(data = "Data loaded"),
        Error(code = 404, message = "Not found")
    )

    val jsonString = json.encodeToString(responses)
    println(jsonString)
    /*
    [
        {"result_type":"Success","data":"Data loaded"},
        {"result_type":"Error","code":404,"message":"Not found"}
    ]
    */

    val decoded = json.decodeFromString<List<Response>>(jsonString)
    when (val first = decoded[0]) {
        is Success -> println("Success: ${first.data}")
        is Error -> println("Error: ${first.code}")
    }
}

Sealed class의 다형성 시리얼라이제이션은 서버가 다양한 응답 유형을 반환하는 API 클라이언트에서 특히 유용합니다. kotlinx.serialization이 없으면 판별자 필드에 대한 when 문을 통한 수동 디시리얼라이저를 작성해야 했습니다. 라이브러리를 사용하면 하나의 어노테이션으로 가능합니다. classDiscriminator를 사용하면 마커 필드의 이름(기본값 “type”)을 서버가 기대하는 임의의 값으로 변경할 수 있습니다.

kotlinx.serialization 어노테이션: 완전 가이드

라이브러리는 시리얼라이제이션을 세밀하게 조정하기 위한 어노테이션 세트를 제공합니다. 기본적인 것은 클래스용 @Serializable입니다. 추가 어노테이션: JSON에서 필드 이름을 설정하는 @SerialName(Kotlin 이름과 다른 경우), 시리얼라이제이션에서 필드를 제외하는 @Transient, JSON에 필수로 존재해야 하는 필드용 @Required, 기본값이 있던 없던 필드를 강제로 시리얼라이즈하는 @EncodeDefault가 있습니다.

어노테이션목적예시
@Serializable클래스에 대한 시리얼라이저 생성 활성화@Serializable data class User
@SerialName형식에서 필드의 대체 이름 설정@SerialName(“user_name”) val name: String
@Transient시리얼라이제이션에서 필드 제외@Transient val cache: MutableMap
@Required디시리얼라이제이션 시 JSON에서 필드 필수@Required val id: String
@EncodeDefault기본값이 있던 시리얼라이즈 필드@EncodeDefault val type: Type = Type.A
@Serializer커스텀 시리얼라이저를 클래스에 바인드@Serializer(forClass = Date::class)

@SerialName 어노테이션은 필드 이름이 snake_case이면서 Kotlin 스타일이 camelCase인 API를 다룰 때 중요합니다. 예를 들어, 서버가 “user_id”를 보내는 반면 Kotlin 코드는 userId를 사용합니다. @SerialName(“user_id”)은 추가 매핑 없이 이 문제를 해결합니다. @Transient는 서버에 보내지 않아야 할 필드(임시 계산값 또는 캐시)에 유용합니다.

Nullable 필드의 대체로서의 @Required

기본적으로 kotlinx.serialization에서 모든 필드는 필수입니다. 필드가 JSON에 없을 수 있다면 nullable(String?)으로 만들거나 기본값을 설정합니다(val name: String = “”). 그러나 Kotlin에서는 non-nullable이지만 API 버전업으로 인해 JSON에 없을 수 있는 필드가 있습니다. 이 경우 @Required는 필드가 없을 때 SerializationException을 발생시키는 반면, 기본값은 오류 없이 채우게 됩니다.

커스텀 시리얼라이저: KSerializer 및 수동 제어

KSerializer는 kotlinx.serialization에서 모든 시리얼라이저가 구현하는 인터페이스입니다. 표준 코드 생성이 적합하지 않은 경우(Date, Bitmap 또는 특정 바이너리 형식 처리 등) 직접 시리얼라이저를 작성할 수 있습니다. 이를 위해 serialize()와 deserialize() 메소드를 구현하고, 형식 스키마에 대한 구조 설명인 디스크립터를 제공합니다.

커스텀 시리얼라이저는 두 가지 방법으로 연결됩니다: @Serializable(with = MySerializer::class) 어노테이션을 통해 특정 클래스에 바인드하거나, Json { serializersModule = ... }를 통해 글로벌하게 모든 인스턴스에 바인드합니다. 두 번째 방법은 내장 타입(Date, UUID)에 대해 모든 필드에 어노테이션을 작성하지 않도록 하므로 더 좋습니다.

kotlin
// java.util.Date에 대한 커스텀 시리얼라이저
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

object DateSerializer : KSerializer<Date> {
    private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)

    override val descriptor: SerialDescriptor =
        PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: Date) {
        encoder.encodeString(dateFormat.format(value))
    }

    override fun deserialize(decoder: Decoder): Date {
        return dateFormat.parse(decoder.decodeString())
    }
}

// 커스텀 시리얼라이저 사용
@Serializable
data class Event(
    val title: String,
    @Serializable(with = DateSerializer::class)
    val date: Date
)

fun main() {
    val json = Json { prettyPrint = true }
    val event = Event("Release", Date())
    println(json.encodeToString(event))
}

이 예제에서 DateSerializer는 java.util.Date를 ISO 8601 문자열로 변환합니다. 커스텀 시리얼라이저 없이는 kotlinx.serialization이 Date를 처리할 수 없습니다 — 이는 Kotlin 표준 라이브러리에 포함되지 않은 타입입니다. 특정 필드에 @Serializable(with = DateSerializer::class)를 적용하면 해당 필드에만 시리얼라이저가 적용됩니다. 모든 Date를 글로벌 등록하려면 Json { serializersModule = SerializersModule { contextual(DateSerializer) } }를 사용하세요.

시리얼라이제이션 형식: JSON, ProtoBuf, CBOR, HOCON

kotlinx.serialization은 JSON에 그쳐있지 않습니다. 이 라이브러리는 4가지 내장 형식을 지원하며, 각각 고유한 모듈과 구성이 있습니다. JSON(kotlinx-serialization-json)은 보편적이고 읽기 쉬우며 REST API에 적합합니다. ProtoBuf(kotlinx-serialization-protobuf)는 바이너리, 압축되었고 필수 스키마가 있으며, 고부하 마이크로서비스에 적합합니다. CBOR(kotlinx-serialization-cbor)은 JSON의 바이너리 응이며, 대역폭이 제한된 IoT 및 모바일 장치에 편리합니다. HOCON(kotlinx-serialization-hocon)은 TypeSafe Config와 호환되는 구성 형식입니다.

형식모듈유형스키마일반적인 사용
JSONkotlinx-serialization-json텍스트선택REST API, 데이터 저장
ProtoBufkotlinx-serialization-protobuf바이너리필수 (.proto)마이크로서비스, gRPC
CBORkotlinx-serialization-cbor바이너리선택IoT, 모바일 장치
HOCONkotlinx-serialization-hocon텍스트선택구성 파일

ProtoBuf는 .proto 파일에 스키마가 필요하지만, kotlinx-serialization-protobuf는 .proto 없이 @Serializable에서 직접 Kotlin 클래스를 생성합니다. 이는 개발을 간소화합니다: data class를 어노테이트하고 ProtoBuf.encodeToByteArray()를 사용하기만 하면 됩니다. CBOR은 NFC 또는 BLE를 통해 압축된 바이너리 데이터를 전송해야 하는 Android 프레임워크에서 특히 중요합니다. 동일한 데이터 세트에서 CBOR 메시지는 JSON보다 평균 20–30% 작습니다.

프로젝트에 맞는 형식 선택

모바일 액의 REST API에는 JSON이 가장 좋은 선택입니다 — 추가 도구 없이 디버그할 수 있고, 로그에서 읽을 수 있으며 어떤 백엑드와도 호환됩니다. 액이 마이크로서비스 간에 큰 양의 데이터(수백 메가바이트)를 전송하는 경우 ProtoBuf는 바이너리 인코딩으로 최대 5x의 속도 이점을 제공합니다. 파일에 설정을 저장하려면 HOCON 또는 JSON을 사용하세요. 트래픽 제한이 엄격한 장치(IoT 센서)에는 CBOR을 사용하세요.

kotlinx.serialization 사용 시 일반적인 실수

첫 번째 실수는 디시리얼라이제이션 시 알 수 없는 키를 무시하는 것입니다. 서버가 새 필드를 추가하고 ignoreUnknownKeys = false일 경우 애플리케이션은 SerializationException으로 충돌합니다. 이 플래그는 기본적으로 비활성화되어 있습니다. 해결 방법: API 변경에 대비하여 프로덕션 코드에서 항상 Json { ignoreUnknownKeys = true }를 설정하세요.

둘째 실수는 data class에서 internal 또는 private 필드를 시리얼라이즈하는 것입니다. Kotlin data class에서 기본 생성자의 모든 필드는 기본적으로 시리얼라이즈됩니다. 필드에 민감한 데이터(비밀번호, 토큰)가 포함된 경우 @Transient로 표시하거나 기본 생성자에서 제외해야 합니다. @Transient는 필드를 JSON에서 완전히 제외하지만, 생성자 내에서는 오류를 일으킬 수 있으므로 이러한 필드는 @Transient와 함께 클래스 본문에 정의하는 것이 좋습니다.

셸째 실수는 sealed class 없이 다형성 시리얼라이제이션을 사용하는 것입니다. sealed 대신 open class를 사용하는 경우 kotlinx.serialization은 serializersModule에서 모든 하위 클래스의 명시적 등록이 필요합니다. 컴파일러가 모든 하위 클래스를 알 수 있는 sealed class와 달리, open class는 임의적인 확장을 허용하므로 라이브러리가 자동으로 모든 하위 타입을 판별할 수 없습니다. 등록은 Json { serializersModule = SerializersModule { polymorphic(Base::class) { subclass(Derived::class) } } }를 통해 수행됩니다.

라이브러리 버전 불일치 오류

kotlinx.serialization의 버전은 Kotlin 버전과 호환되어야 합니다. JetBrains는 호환성 표를 공개합니다: kotlinx-serialization 1.6.x는 Kotlin 1.9.x와, 1.7.x는 Kotlin 2.0.x 및 2.1.x와 호환됩니다. 버전 불일치는 “Symbol ‘serializer’ is missing”가ᧇ 알 수 없는 컴파일 오류를 일으킵니다. Maven Central 또는 프로젝트의 GitHub 리포지토리에서 최신 버전을 항상 확인하세요.

자주 묻는 질문

kotlinx.serialization은 Gson과 Moshi와 어떻게 다름니까?

kotlinx.serialization은 KSP를 통한 컴파일 시간 코드 생성을 사용하는 반면, Gson과 Moshi는 실행 시간 리플렉션을 사용합니다. 이를 통해 성능 이점(Gson보다 3–5배 빠름)과 타입 안전성을 제공합니다. Gson은 어노테이션 없이 모든 필드를 시리얼라이즈하여 데이터 누숙을 유발할 수 있습니다. kotlinx.serialization은 명시적 @Serializable 어노테이션이 필요하므로 더 안전합니다. Moshi는 codegen을 지원하지만 JVM과 Android만 가능합니다.

kotlinx.serialization은 Kotlin Multiplatform을 지원합니까?

, kotlinx.serialization은 JetBrains의 공식 멀티플랫폼 라이브러리입니다. Kotlin/JVM(Android, Backend), Kotlin/Native(iOS), Kotlin/JS(Web, React) 및 Kotlin/Wasm에서 동작합니다. API는 모든 플랫폼에서 통일되어 있습니다: @Serializable + Json.encodeToString()은 어디서나 동일하게 작동합니다. iOS에 대해 추가 설정이 필요하지 않습니다 — Kotlin/Native가 시리얼라이즈된 코드를 네이티브 바이너리로 컴파일합니다.

JSON에서 null 필드는 어떻게 처리됩니까?

Nullable 필드(String?)는 JSON에서 값이 없거나 null인 경우 null로 디시리얼라이즈됩니다. 기본값이 없는 non-nullable 필드(String)의 경우 JSON에서 필드가 없으면 SerializationException이 발생합니다. null 값이 JSON에 나타나지 않기를 바란다면 Json { encodeDefaults = false }를 구성하세요. 이는 기본값과 같은 모든 필드를 제외합니다(nullable 타입의 null 포함).

서버가 snake_case 필드를 보내면 어떻게 하나요?

사용하세요: 이름이 Kotlin 형식과 다른 각 필드에 @SerialName(“snake_case_name”)을 적용하세요. 대신 Kotlin 2.0+에서는 Json { namingStrategy = JsonNamingStrategy.SnakeCase }가 자동 camelCase ↔ snake_case 변환을 위해 제공됩니다. 이 설정은 모든 필드에 한꺼번에 적용됩니다. 부분적인 커스터마이징이 필요하면 @SerialName을 글로벌 전략과 결합하세요.

Kotlin Flow나 코루틴을 시리얼라이즈할 수 있나요?

아니요, Flow와 코루틴은 직접 시리얼라이즈할 수 없습니다 — 이들은 비동기 실행을 나타내며, 데이터가 아닙니다. Flow에서 데이터를 전송하려면 코루틴 내에서 .toList()를 통해 컬렉션으로 수집하고 해당 컬렉션을 시리얼라이즈합니다. 마찬가지로 Job, Deferred 또는 Continuation도 시리얼라이즈할 수 없습니다. 오직 data class만 시리얼라이즈하세요 — 데이터 모델이며 행위 로직이 없는 것입니다.

요약

  • kotlinx.serialization — @Serializable를 통한 컴파일 시간 시리얼라이제이션, 리플렉션 없음, Gson보다 최대 5배 빠름
  • @Serializable, @SerialName, @Transient — 필드와 클래스 시리얼라이제이션을 구성하는 주요 어노테이션
  • Json {} builder JSON 구성: ignoreUnknownKeys, prettyPrint, coerceInputValues, encodeDefaults
  • Sealed class 및 다형성 시리얼라이제이션 — 추가 코드 없이 타입 계층 원활한 지원
  • KSerializer — 비표준 타입(Date, Bitmap, UUID)의 커스텀 시리얼라이저 인터페이스
  • 4가지 형식: JSON, ProtoBuf, CBOR, HOCON — 모듈로 추가, 모든 형식에 통일 API
  • 멀티플랫폼 — JVM, Native, JS 및 Wasm을 위한 단일 코드베이스; KMM 및 공유 모듈에 필수

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기