[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 function
과 refiled
키워드를 사용하여 이러한 요구조건을 지원하고 있는데, 이것을 사용하면 모든 방식을 사용할 수 있게 됩니다.
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
키워드는 문법
및 성능
에 제약이 있기 때문에, 해당 키워드가 적용되는 구간(함수)를 최대한 짧게 정의하는 것이 좋습니다.