I/O 소스로 JSON 직렬화하기

I/O 소스로 JSON 직렬화하기

Kotlin 직렬화 라이브러리는 JVM 스트림과 kotlinx-io 또는 Okio의 source·sink를 다루는 API를 제공해요.

이 API를 쓰면 중간 문자열을 만들지 않고 I/O 소스에서 직접 JSON을 직렬화·역직렬화할 수 있어요. 이 API는 UTF-8 인코딩을 사용하며, 잘못된 JSON 데이터에는 SerializationException, I/O 실패에는 IOException을 던집니다.

I/O 리소스를 다룰 때는 자원 누수를 막기 위해 제대로 닫아 주는 게 중요해요. .use() 함수를 쓰면 작업이 끝날 때 리소스를 자동으로 닫아 줍니다.

출처: Serialize JSON with I/O sources

본문

JSON을 JVM 출력 스트림으로 직렬화하기

.encodeToStream() 확장 함수를 쓰면 JSON을 JVM OutputStream에 직접 직렬화할 수 있어요.

// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import java.io.FileOutputStream

@Serializable
data class Project(val name: String, val stars: Int)

fun main() {
    val project = Project("kotlinx.serialization", 9000)
    
    // Creates an OutputStream for the project.json file
    FileOutputStream("project.json").use { output ->
        
        // Serializes the project instance into the OutputStream
        Json.encodeToStream(project, output)
    }
}

이 예시에서 Project의 JSON 표현이 project.json 파일로 직렬화돼요.

JSON을 JVM 입력 스트림에서 역직렬화하기

JVM InputStream에서 JSON을 직접 역직렬화하려면 .decodeFromStream() 확장 함수를 사용하면 됩니다.

// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import java.io.FileInputStream

@Serializable
data class Project(val name: String, val stars: Int)

fun main() {
    // Opens an InputStream
    FileInputStream("project.json").use { input ->

        // Deserializes the JSON contents of the InputStream into a Project instance
        val project = Json.decodeFromStream<Project>(input)

        // Prints the deserialized Project instance
        println(project)
    }
}

이 예시에서 입력 스트림의 JSON 내용은 하나의 Project 인스턴스로 역직렬화돼요.

입력이 최상위 JSON 배열이나 공백으로 구분된 객체들 속에 여러 JSON 객체를 담고 있다면, .decodeToSequence()로 요소들을 지연(lazily) 처리할 수 있어요. 이렇게 하면 파싱되는 대로 각 값을 처리할 수 있죠. 예를 들면 다음과 같아요.

// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import java.io.FileInputStream

@Serializable
data class Project(val name: String, val stars: Int)

fun main() {
    // Opens an InputStream for the projects.json file containing a JSON array of Project objects
    FileInputStream("projects.json").use { input ->

        // Lazily deserializes each Project from the InputStream
        val projects = Json.decodeToSequence<Project>(input)

        // Processes elements one by one
        for (project in projects) {
            println(project)
        }
    }
}

.decodeToSequence()가 반환한 시퀀스는 내부적으로 기본 스트림과 연결되어 있어서 한 번만 반복할 수 있어요.

시퀀스를 완전히 평가하기 전에 내부 스트림이 닫히면, 시퀀스에 접근할 때 예외가 발생할 수 있습니다.

kotlinx-io와 Okio로 JSON 직렬화하기

JVM 스트림 외에도 kotlinx.io.Sinkkotlinx.io.Source(kotlinx-io 라이브러리의 타입, 현재 Alpha 단계)나 okio.BufferedSinkokio.BufferedSource(Okio 라이브러리의 타입) 같은 I/O 타입으로도 JSON을 다룰 수 있어요.

다음 Json 확장 함수들을 사용해 이런 I/O 타입으로 JSON을 직접 읽고 쓸 수 있습니다.

다음 절들에서는 kotlinx-io 타입을 이 API와 함께 쓰는 예시를 다룰게요. Okio 타입도 해당 okio.BufferedSink·okio.BufferedSource API로 비슷하게 사용할 수 있어요.

kotlinx-io와 Okio 의존성 추가하기

kotlinx-io나 Okio 타입과 함께 확장 함수를 쓰려면 해당 의존성을 추가해야 해요.

kotlinx-io 의존성 추가하기

// build.gradle(.kts)
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-io:1.11.0")
    implementation("org.jetbrains.kotlinx:kotlinx-io-core:0.9.1")
}
<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.jetbrains.kotlinx</groupId>
        <artifactId>kotlinx-serialization-json-io</artifactId>
        <version>1.11.0</version>
    </dependency>
    <dependency>
        <groupId>org.jetbrains.kotlinx</groupId>
        <artifactId>kotlinx-io-core</artifactId>
        <version>0.9.1</version>
    </dependency>
</dependencies>

Okio 의존성 추가하기

// build.gradle(.kts)
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.11.0")
    implementation("com.squareup.okio:okio:3.16.2")
}
<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.jetbrains.kotlinx</groupId>
        <artifactId>kotlinx-serialization-json-okio</artifactId>
        <version>1.11.0</version>
    </dependency>
    <dependency>
        <groupId>com.squareup.okio</groupId>
        <artifactId>okio</artifactId>
        <version>3.16.2</version>
    </dependency>
</dependencies>

Sink로 JSON 직렬화하기

Sink로 JSON을 직렬화하려면 .encodeToSink() 함수를 사용하면 돼요.

// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*

// Imports declarations for kotlinx-io types and JSON I/O support
import kotlinx.serialization.json.io.*
import kotlinx.io.*
import kotlinx.io.files.*

@Serializable
data class Project(val name: String, val stars: Int)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    val project = Project("kotlinx.serialization", 9000)

    // Creates a Sink for the project.json file
    val path = Path("project.json")
    SystemFileSystem.sink(path).buffered().use { sink: Sink ->

        // Serializes the Project instance directly into a Sink
        Json.encodeToSink(project, sink)
    }
}

Source에서 JSON 역직렬화하기

Source에서 JSON을 역직렬화하려면 .decodeFromSource() 함수를 사용하면 돼요.

// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*

// Imports declarations for kotlinx-io types and JSON I/O support
import kotlinx.serialization.json.io.*
import kotlinx.io.*
import kotlinx.io.files.*

@Serializable
data class Project(val name: String, val stars: Int)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    // Opens a Source for the project.json file
    val path = Path("project.json")
    SystemFileSystem.source(path).buffered().use { source: Source ->

        // Deserializes a Project instance directly from a Source
        val project = Json.decodeFromSource<Project>(source)

        println(project)
    }
}

입력이 큰 JSON 배열이나 여러 최상위 JSON 객체를 담고 있다면, .decodeSourceToSequence() 함수로 Source를 지연 디코딩되는 Sequence<T>로 바꿀 수 있어요.

// Imports declarations from the serialization library
import kotlinx.serialization.*
import kotlinx.serialization.json.*

// Imports declarations for kotlinx-io types and JSON I/O support
import kotlinx.serialization.json.io.*
import kotlinx.io.*
import kotlinx.io.files.*

@Serializable
data class Project(val name: String, val stars: Int)

@OptIn(ExperimentalSerializationApi::class)
fun main() {
    // Opens a Source for the projects.json file containing multiple JSON objects
    val path = Path("projects.json")
    SystemFileSystem.source(path).buffered().use { source: Source ->

        // Lazily deserializes each Project as it is read from the Source
        val projects: Sequence<Project> = Json.decodeSourceToSequence(source)

        for (project in projects) {
            println(project)
        }
    }
}

.decodeSourceToSequence()가 반환한 시퀀스는 내부적으로 기본 Source와 연결되어 있어서 한 번만 반복할 수 있어요.

시퀀스를 완전히 평가하기 전에 내부 source가 닫히면, 시퀀스에 접근할 때 예외가 발생할 수 있습니다.

더 알아보기