본문 바로가기

카테고리 없음

Kotlin JSON 변환/파싱 라이브러리 - Jackson

[Change Log]
2018-12-02 : 게시글 작성
2020-07-04 : 디자인 깨짐현상 수정


Jackson

Jackson라이브러리는 코틀린 객체JSON간의 변환/파싱을 담당합니다. 이것은 프로그래머가 문서에서 값을 파싱하여 객체에 일일이 붙여넣지 않아도 된다는 것을 의미하므로, 정신건강에 매우 이로운 라이브러리라 할 수 있습니다.


설치하기

Jackson을 사용하기 위해서는 의존성을 빌드 스크립트에 기술해야 합니다. Gradle또는 Maven에 의존성을 적으면 다음 동기화 시점에 Jackson을 자동으로 다운로드 합니다. 이후에 버전이 업그레이드될 수 있으므로 최신 버전은 레포지터리를 참조해주세요.


for Gradle

compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.+"

for Maven

<dependency>
    <groupid>com.fasterxml.jackson.module</groupid>
    <artifactid>jackson-module-kotlin</artifactid>
    <version>2.9.7</version>
</dependency>

Hello, Jackson!

아래의 코드가 성공적으로 컴파일되었다면 Jackson이 정상적으로 설치된 것 입니다.

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
fun main(args: Array<string>)
{
    val mapper = jacksonObjectMapper()
    println("Hello, Jackson!")
}


직렬화

객체 직렬화

객체JSON으로 변환하는 것을 직렬화라고 합니다. 이것을 실험하기에 앞서 간단한 테스트용 클래스를 작성해보겠습니다. 강제는 아니지만 코틀린의 Data Class문법을 사용하면 코드가 한결 간결해집니다.

import java.time.LocalDateTime

data class Article(
    // 게시글 제목
    val title : String = "",

    // 게시글 등록일자
    val date  : LocalDateTime? = null,

    // 게시글 조회회수
    val viewCnt  : Int = 0,

    // 게시글 내용
    val content : String = ""
)

객체JSON File로 저장하고 싶다면 JSON Printer를 가져온 뒤 writeValue(file_path, object)메소드를 사용하면 됩니다.

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.io.File

fun main(args: Array<String>)
{
    val mapper  = jacksonObjectMapper()
    val article = Article(
        "MyArticle", 
        System.currentTimeMillis(), 
        0, 
        "Hello, Jackson!"
    )

    // Object to JSON.
    mapper
        .writerWithDefaultPrettyPrinter()
        .writeValue(
            File("./my_article.json"),
            article
        )
}

위의 결과로 ./my_article.json 경로에 아래와 같은 JSON File이 생성됩니다.

{
  "title" : "MyArticle",
  "date" : 1544109331398,
  "viewCnt" : 0,
  "content" : "Hello, Jackson!"
}

컬렉션 직렬화

컬렉션도 하나의 객체이므로 오브젝트를 직렬화하듯이 사용하면 됩니다.

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.io.File

fun main(args: Array<String>)
{
    val mapper   = jacksonObjectMapper()
    val articles = ArrayList<Article>()

    // Add elem to collection.
    (1..3).forEach { n ->
        articles.add(
            Article(
                "MyArticle $n",
                System.currentTimeMillis(),
                0,
                "Hello, Jackson! $n"))
    }


    // Convert Collection to JSON.
    mapper
        .writerWithDefaultPrettyPrinter()
        .writeValue(
            File("./my_articles.json"),
            articles
        )
}

ArrayList<T>는 아래와 같이 직렬화됩니다.

[ {
  "title" : "MyArticle 1",
  "date" : 1544109594914,
  "viewCnt" : 0,
  "content" : "Hello, Jackson! 1"
}, {
  "title" : "MyArticle 2",
  "date" : 1544109594914,
  "viewCnt" : 0,
  "content" : "Hello, Jackson! 2"
}, {
  "title" : "MyArticle 3",
  "date" : 1544109594914,
  "viewCnt" : 0,
  "content" : "Hello, Jackson! 3"
} ]

역직렬화

JSON File객체로 파싱하는 것을 역직렬화라고 합니다. Jackson이 역직렬화를 수행하기 위해서는 어떤 타입으로 변환할 것인지에 대한 정보를 건네주어야 합니다. Jackson이 해석할 수 있는 클래스 정보는 다음과 같습니다.

  • Class<T>
  • TypeReference<T>

Class<T>는 자바에서 만들어진 객체이지만 TypeReference<T>Jackson에서 만들어진 Type Metadata 객체입니다. 다만 TypeReference<T>추상 데이터 타입이기 때문에 빈 오브젝트에 상속시켜 구체화해야 합니다. 이렇게 만들어진 타입정보를 건네주면 건네준 타입으로 역직렬화를 수행합니다.


객체로 역직렬화

JSON File객체로 변환하고 싶다면 Mapper.readValue<T>(file, type) : T 메소드를 사용하면 됩니다. 인자 중에서 type은 당연하게도 T에 대한 메타정보객체여야 합니다. 단, 제네릭에서 데이터 타입을 유추할 수 있는 경우 type은 생략할 수 있습니다.

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import java.io.File

fun main(args: Array<String>)
{
    val mapper  = jacksonObjectMapper()
    val article = mapper.readValue<Article>(File("./my_article.json"))

    println(article)
}


컬렉션으로 역직렬화

역시나 기본적은 것은 객체를 역직렬화 할 때와 동일합니다. 다만, 데이터 타입을 Collection으로 한번 감싼것만 다릅니다.

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import java.io.File

fun main(args: Array<String>)
{
    val mapper= jacksonObjectMapper()
    val articles = mapper.readValue<ArrayList<Article>>(File("./my_articles.json"))

    println(articles)
}


제네릭 이슈

타입 메타정보를 생성하기 위한 3가지 방법을 다시 상기해봅시다.


Jackson이 역직렬화를 하기 위해서는 T에 대한 메타정보가 필요하다고 설명했습니다. 그러나 코틀린의 설계상 제네릭 타입 T는 런타임 시점에서 추상화되기 때문에 T의 내부적인 정보에 접근하는 것이 불가능합니다. 즉, 런타임 시점에서 T의 내부적 정보가 지워지므로 위의 타입전달 방식에서 1번2번은 사용할 수 없습니다.

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.io.File

fun main(args: Array<String>)
{
    val article = read<Article>("./my_article.json")
    println(article)
}

fun <T> read(filePath:String) : T
{
    val mapper= jacksonObjectMapper()
    val file = File(filePath)
    val obj = mapper.readValue<T>(file, T::class.java)  // Error!
    return obj
}

그렇다고 3번도 유효하지는 않습니다. 해당 방식으로 타입정보를 넘겨서 역직렬화를 수행하면 아래와 같은 익셉션이 발생하면서 종료됩니다.

class java.util.LinkedHashMap cannot be cast to class Article

에러메세지는 LinkedHashMap객체를 Article객체로 캐스팅할 수 없다고 말하고 있습니다. 어디가 문제인 걸까요?

import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.io.File

fun main(args: Array<String>)
{
    // java.lang.ClassCastException :
    // class java.util.LinkedHashMap cannot be cast to class Article
    val article = read<Article>("./my_article.json")

    println(article)
}

fun <T> read(filePath:String) : T
{
    val mapper= jacksonObjectMapper()
    val file = File(filePath)
    val obj = mapper.readValue<T>(file, object:TypeReference<T>(){})
    return obj   // TypeOf(obj) == LinkedHashMap
}

문제는 T가 런타임 시점에서 추상화된다는 것에 있습니다. T에 대한 내부정보가 사라졌기 때문에 TypeReference의 상속이 우리의 예상대로 이루어지지 않은 것이죠. 이런 불완전한 TypeReference를 받은 Jackson은 어떠한 타입으로 변환해야 할지 모르기 때문에 임시방편으로 LinkedHashMap으로 변환하고, 이후에 Article변수에 대입하기 위해 캐스팅을 시도하지만 실패하는 것이죠.


이러한 이슈를 해결하려면 T에 대한 정보가 런타임 시점에도 유지되게끔 해야 합니다. 코틀린은 inline functionrefiled키워드를 사용하여 이러한 요구조건을 지원하고 있는데, 이것을 사용하면 모든 방식을 사용할 수 있게 됩니다.

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.io.File

fun main(args: Array<String>)
{
    val article = read<Article>("./my_article.json")
    println(article)
}

//
// inline + reified
inline fun <reified T> read(filePath:String) : T
{
    val mapper= jacksonObjectMapper()
    val file = File(filePath)
    val obj = mapper.readValue<T>(file, T::class.java)
    return obj
}


다만 refiled 키워드는 문법성능에 제약이 있기 때문에, 해당 키워드가 적용되는 구간(함수)를 최대한 짧게 정의하는 것이 좋습니다.