10. 애노테이션과 리플렉션
애노테이션을 사용하면 라이브러리가 요구하는 의미를 클래스에게 부여할 수 있고, 리플렉션을 사용하면 실행 시점에 컴파일러 내부 구조를 분석할 수 있다. 코틀린에서 애노테이션을 사용하는 문법은 자바와 똑같지만 애노테이션을 선언할 때 사용하는 문법은 자바와 약간 다르다. 리플렉션 API의 일반 구조는 자바와 같지만 세부 사항에서 약간 차이가 있다.
10장에서는 실전 프로젝트에 준하는 JSON 직렬화와 역직렬화 라이브러리인 제이키드를 구현한다. 이 라이브러리는 실행 시점에 코틀린 객체의 프로퍼티를 읽거나 JSON 파일에서 읽은 데이터를 코틀린 객체로 만들기 위해 리플렉션을 사용한다. 그리고 애노테이션을 통해 제이키드 라이브러리 클래스와 프로퍼티를 직렬화하고 역직렬화하는 방식을 변경한다.
10.1 애노테이션 선언과 적용
10.1.1 애노테이션 적용
코틀린에서는 자바와 같은 방법으로 애노테이션을 사용할 수 있다. 애노테이션은 @
과 애노테이션 이름으로 이뤄진다. 함수나 클래스 등 여러 다른 코드 구성 요소에 애노테이션을 붙일 수 있다.
자바와 코틀린에서 @Deprecated
애노테이션의 의미는 똑같다. 하지만 코틀린에서는 replaceWith
파라미터를 통해 옛 버전을 대신할 수 있는 패턴을 제시할 수 있고, API 사용자는 그 패턴을 보고 지원이 종료된 API 기능을 더 쉽게 새 버전으로 포팅할 수 있다. 사용 금지를 설명하는 메시지와 대체할 패턴을 지정하면 된다.
@Deprecated("Use removeAt(index) instead.", ReplaceWith("removeAt(index)"))
fun remove(index: Int) { ... }
위와 같이 선언된 remove
라는 함수 선언이 있다면 인텔리J는 remove
를 호출하는 코드에 대해 경고메시지를 표시해 줄 뿐 아니라 자동으로 그 코드를 새로운 API 버전에 맞는 코드로 바꿔주는 퀵 픽스도 제시해준다.
애노테이션의 인자로는 원시 타입의 값, 문자열, enum, 클래스 참조, 다른 애노테이션 클래스, 그리고 지금까지 말한 요소들로 이뤄진 배열이 들어갈 수 있다.
- 클래스를 애노테이션 인자로 지정할 때는
@MyAnnotation(MyClass::class)
처럼 ::class를 클래스 이름 뒤에 넣어야 한다. - 다른 애노테이션을 인자로 지정할 때는 인자로 들어가는 애노테이션의 이름 앞에 @를 넣지 않아야 한다. 예를 들어 방금 살펴본 예제의
ReplaceWith
는 애노테이션이지만Deprecated
애노테이션의 인자로 들어가므로ReplaceWith
앞에 @를 사용하지 않는다. - 배열을 인자로 지정하려면
@RequestMapping(oath = arrayOf("/foo", "/bar"))
처럼 arrayOf 함수를 사용한다.
애노테이션 인자를 컴파일 시점에 알 수 있어야 하기 때문에 임의의 프로퍼티를 인자로 지정할 수는 없다. 프로퍼티를 애노테이션 인자로 사용하려면 그 앞에 const
변경자를 붙여야 한다.
const val TEST_TIMEOUT = 100L
@Test(timeout = TEST_TIMEOUT) fun testMethod() { ... }
10.1.2 애노테이션 대상
- 사용 지점 대상 선언으로 애노테이션을 붙일 요소를 정할 수 있다.
@get:Rule
사용지점 대상은 @ 기호와 애노테이션 이름 사이에 붙으며, 애노테이션 이름과는 콜론(:)으로 분리된다. get은 @Rule 애노테이션을 프로퍼티 게터에 적용하라는 뜻이다.
자바에 선언된 애노테이션을 사용해 프로퍼티에 애노테이션을 붙이는 경우 기본적으로 프로퍼티의 필드에 그 애노테이션이 붙지만 코틀린으로 애노테이션을 선언하면 프로퍼티에 직접 적용할 수 있는 애노테이션을 만들 수 있다.
사용 지점 대상을 지정할 때 지원하는 대상 목록
- property : 프로퍼티 전체, 자바에서 선언된 애노테이션에는 이 사용 지점 대상을 사용할 수 없다.
- field
- get
- set
- receiver
- param
- setparam
- delegate
- file
10.1.3 애노테이션을 활용한 JSON 직렬화 제어
지금부터 JSON 직렬화를 위한 제이키드라는 순수 코틀린 라이브러리를 구현하는 과정을 알아본다.
@JsonExclude
애노테이션을 사용하면 직렬화나 역직렬화 시 그 프로퍼티를 무시할 수 있다.@JsonName
애노테이션을 사용하면 프로퍼티를 표현하는 키/값 쌍의 키로 프로터리 이름 대신 애노테이션이 지정한 이름을 쓰게 할 수 있다.
data class Person(
@JsonName("alias") val firstName: String,
@JsonExclude val age: Int? = null
)
firstName 프로퍼티를 JSON으로 저장할 때 사용하는 키를 변경하기 위해 @JsonName
애노테이션을 사용하고, age
프로퍼티를 직렬화나 역직렬화에 사용하지 않기 위해 @JsonExclude
애노테이션을 사용한다. 직렬화 대상에서 제외할 age
프로퍼티에는 반드시 디폴트 값을 지정해야만 한다. 디폴트 값을 지정하지 않으면 역직렬화 시 Person
의 인스턴스를 새로 만들 수 없다.
지금까지 제이키드 라이브러리가 제공하는 기능인 serialize(), deserialize(), @JsonName, @JsonExclude
를 살펴봤다. 이제 제이키드 구현을 살펴보자.
10.1.4 애노테이션 선언
@JsonExclude
애노테이션은 아무 파라미터도 없는 가장 단순한 애노테이션이다.
annotation class JsonExclude
이 애노테이션 선언의 일반 클래스와의 차이는 class
키워드 앞에 annotation
이라는 변경자가 붙어있다는 점 뿐인 것 같지만 애노테이션 클래스는 오직 선언이나 식과 관련 있는 메타데이션의 구조를 정의하기 때문에 내부에 아무 코드도 들어있을 수 없다. 그런 이유로 컴파일러는 애노테이션 클래스에서 본문을 정의하지 못하게 막는다.
파라미터가 있는 애노테이션을 정의하려면 애노테이션 클래스의 주 생성자에 파라미터를 선언해야 한다. 다만 애노테이션 클래스에서는 모든 파라미터 앞에 val
을 붙여야만 한다.
// kotlin
annotation class JsonName(val name: String) // name 프로퍼티 사용
// java
public @interface JsonName {
String value(); // value 메소드 사용.
}
- 자바에서 value 메소드는 특별하다. 어떤 애노테이션을 적용할 때 value를 제외한 모든 애트리뷰트에는 이름을 명시해야 한다.
- 코틀린의 애노테이션 적용 문법은 일반적인 생성자 호출과 같다. 따라서 인자의 이름을 명시하기 위해 이름 붙인 인자 구문을 사용할 수도 있고 이름을 생략할 수도 있다. 여기서는
name
이JsonName
생성자의 첫 번째 인자이므로JsonName(name = "first_name")
은JsonName("first_name")
과 같다. - 자바에서 선언한 애노테이션을 코틀린의 구성 요소에 적용할 때는 value를 제외한 모든 인자에 대해 이름 붙인 인자 구문을 사용해야만 한다.
10.1.5 메타애노테이션: 애노테이션을 처리하는 방법 제어
애노테이션 클래스에 적용할 수 있는 애노테이션을 메타애노테이션이라고 부른다. 표준 라이브러리에 있는 메타애노테이션 중 가장 흔히 쓰이는 메타애노테이션은 @Target
이다. 제이키드의 JsonExclude
와 JsonName
애노테이션도 적용가능 대상을 지정하기 위해 @Target
을 사용한다.
@Target(AnnotationTarget.PROPERTY) // `@Target `메타애노테이션은 애노테이션을 적용할 수 있는 요소의 유형을 지정한다. 구체적인 `@Target`을 지정하지 않으면 모든 선언에 적용할 수 있는 애노테이션이 된다.
annotation class JsonExclude
메타애노테이션을 직접 만들어야 한다면 ANNOTATION_CLASS를 대상으로 지정하라.
@Target(AnnotationTarget.ANNOTATION_CLASS)
annotation class BindingAnnotation
@BindingAnnotation
annotation class MyBinding
대상을 PROPERTY
로 지정한 애노테이션을 자바 코드에서 사용할 수는 없다. 자바에서 그런 애노테이션을 사용하려면 AnnotationTarget.FIELD
를 두 번째 대상으로 추가해야 한다.
10.1.6 애노테이션 파라미터로 클래스 사용
클래스를 선언 메타데이터로 참조할 수 있는 기능이 필요할 때, 클래스 참조를 파라미터로 하는 애노테이션 클래스를 선언하면 그런 기능을 사용할 수 있다. 제이키드 라이브러리의 @DeserializeInterface
는 인터페이스 타입인 프로퍼티에 대한 역직렬화를 제어할 때 쓰는 애노테이션이다. 인터페이스의 인스턴스를 직접 만들 수는 없다. 따라서 역직렬화 시 어떤 클래스를 사용해 인터페이스를 구현할지를 지정할 수 있어야 한다.
interface Company {
val name: String
}
data class CompanyImpl(override val name: String) : Company
data class Person(
val name: String,
@DeserializeInterface(CompanyImpl::class) val company: Company
)
직렬화된 Person
인스턴스를 역직렬화하는 과정에서 company
프로퍼티를 표현하는 JSON을 읽으면 제이키드는 그 프로퍼티 값에 해당하는 JSON을 역직렬화하면서 CompanyImpl
의 인스턴스를 만들어서 Person
인스턴스의 company
프로퍼티에 설정한다.
annotation class DeserializeInterface(val targetClass: KClass<out Any>)
KClass
는 자바java.lang.Class
타입과 같은 역할을 하는 코틀린 타입이다. 코틀린 클래스에 대한 참조를 저장할 때KClass
타입을 사용한다.KClass
의 타입 파라미터는 이KClass
의 인스턴스가 가리키는 코틀린 타입을 지정한다. 예를 들어CompanyImpl::class
의 타입은KClass<CompanyImpl>
이며, 이 타입은 방금 살펴본DeserializeInterface
의 파라미터 타입인KClass<out Any>
의 하위 타입이다.KClass
의 타입 파라미터를 쓸 때out
변경자 없이KClass<Any>
라고 쓰면DeserializeInterface
에게CompanyImpl::class
를 인자로 넘길 수 없고 오직Any::class
만 넘길 수 있다.- 반면
out
키워드가 있으면 모든 코틀린 타입 T에 대해KClass<T>
가KClass<out Any>
의 하위 타입이 된다. 따라서Deserialize Interface
의 인자로Any
뿐 아니라Any
를 확장하는 모든 클래스에 대한 참조를 전달 할 수 있다.
10.1.7 애노테이션 파라미터로 제네릭 클래스 받기
기본적으로 제이키드는 원시 타입이 아닌 프로퍼티를 중첩된 객체로 직렬화한다. 이런 기본 동작을 변경하고 싶으면 값을 직렬화하는 로직을 직접 제공하면 된다.
@CustomSerializer
애노테이션은 커스텀 직렬화 클래스에 대한 참조를 인자로 받는다. 이 직렬화 클래스는 ValueSerializer
인터페이스를 구현해야만 한다.
interface ValueSerializer<T> {
fun toJsonValue(value: T): Any?
fun fromJsonValue(jsonValue: Any?): T
}
날짜를 직렬화한다고 하자. 이때 ValueSerializer<Date>
를 구현하는 DateSerializer
를 사용하고 싶다. 다음은 이 직렬화 로직을 Person
클래스에 적용하는 방법을 보여준다.
data class Person(
val name: String,
@CustomSerializer(DateSerializer::class) val birthDate: Date
)
@CustomSerializer
애노테이션을 구현하는 방법을 살펴보자. ValueSerializer
클래스는 제네릭 클래스라서 타입 파라미터가 있다. 따라서 ValueSerializer
타입을 참조하려면 항상 타입 인자를 제공해야 한다. 하지만 이 애노테이션이 어떤 타입에 대해 쓰일지 전혀 알 수 없으므로 여기서는 스타 프로젝션(*)을 사용할 수 있다.
annotation class CustomSerializer(val serializerClass: KClass<out ValueSerializer<*>>)
CustomSerializer
가 ValueSerializer
를 구현하는 클래스만 인자로 받아야함을 명시할 필요가 있다. 예를 들어 Date
가 ValueSerializer
를 구현하지 않으므로 @CustomSerializer(Date::class)
라는 애노테이션을 금지시켜야 한다.
클래스를 인자로 받아야 하면 애노테이션 파라미터 타입에 KClass<out 허용할 클래스 이름>
을 쓴다.
제네릭 클래스를 인자로 받야야 하면 KClass<out 허용할 클래스 이름<*>>
처럼 허용할 클래스의 이름 뒤에 스타 프로젝션을 덧붙인다.
10.2 리플렉션: 실행 시점에 코를린 객체 내부 관찰
간단히 말해 리플렉션은 실행 시점에 (동적으로) 객체의 프로퍼티와 메소드에 접근할 수 있게 해주는 방법이다.
타입과 관계없이 객체를 다뤄야 하거나 객체가 제공하는 메소드나 프로퍼티 이름을 오직 실행 시점에만 알 수 있는 경우가 있다. JSON 직렬화 라이브러리가 그런 경우다. 이런 경우 리플렉션을 사용해야 한다.
코틀린에서 리플렉션을 사용하려면 두 가지 서로 다른 리플렉션 API를 다뤄야 한다.
- 첫 번째는 자바가
java.lang.reflect
패키지를 통해 제공하는 표준 리플렉션 - 두 번째는 코틀린이
kotlin.reflect
패키지를 통해 제공하는 코틀린 리플렉션 API. 이 API는 자바에는 없는 프로퍼티나 널이 될 수 있는 타입과 같은 코틀린 고유 개념에 대한 리플렉션을 제공한다.
10.2.1 코틀린 리플렉션 API: KClass, KCallable, KFunction, KProperty
코틀린 리플렉션 API를 사용할 때 처음 접하게 되는 것은 클래스를 표현하는 KClass
다. java.lang.Class
에 해당하는 KClass
를 사용하면 클래스 안에 있는 모든 선언을 열거하고 각 선언에 접근하거나 클래스의 상위 클래스를 얻는 등의 작업이 가능하다.
MyClass::class
라는 식을 쓰면 KClass
의 인스턴스를 얻을 수 있다.
아래 예제는 클래스 이름과 그 클래스에 들어있는 프로퍼티 이름을 출력하고 memberProperties
를 통해 클래스와 모든 조상 클래스 내부에 정의된 비확장 프로퍼티를 모두 가져온다.
class Person(val name: String, val age: Int)
>>> import kotlin.reflect.full.*
>>> val person = Person("Alice", 29)
>>> val kClass = person.javaClass.kotlin // KClass<Person>의 인스턴스를 반환
>>> println(kClass.simpleName)
Person
>>> kClass.memberProperties.forEach { println(it.name) }
age
name
KClass
선언을 찾아보면 클래스의 내부를 살펴볼 때 사용할 수 있는 다양한 메소드를 볼 수 있다.
public actual interface KClass<T : Any> {
val simpleName: String?
val qualifiedName: String?
val members: Collection<KCallable<*>>
val constructors: Collection<KFunction<T>>
val nestedClasses: Collection<KClass<*>>
...
클래스의 모든 멤버의 목록이 KCallable
인스턴스의 컬렉션이다. KCallable
은 함수와 프로퍼티를 아우르는 공통 상위 인터페이스다. 그 안에는 call
메소드가 들어있고, call
을 사용하면 함수나 프로퍼티의 게터를 호출할 수 있다.
interface KCallable<out R> {
fun call(vararg args: Any?): R
...
}
call
을 사용할 때는 함수 인자를 vararg
리스트로 전달한다. 다음 코드는 리플렉션이 제공하는 call
을 사용해 함수를 호출할 수 있음을 보여준다.
fun foo(x: Int) = println(x)
>>> val kFunction = ::foo
>>> kFunction.call(42)
42
fun sum(x: Int, y: Int) = x + y
>>> var kFunction: KFunction2<Int, Int, Int> = ::sum
>>> println(kFunction.invoke(1,2) + kFunction(3,4))
10
>>> kFunction(1)
ERROR: No value passed for parameter p2
>>> println(kFunction.invoke("1","2") + kFunction(3,4))
ERROR: Type mismatch.
kFunction
의 invoke
메소드를 호출할 때는 인자 개수나 타입이 맞아 떨어지지 않으면 컴파일이 안된다. 따라서 KFunction
의 인자 타입과 반환 타입을 모두 다 안다면 invoke
메소드를 호출하는 게 낫다. call
메소드는 모든 타입의 함수에 적용할 수 있는 일반적인 메소드지만 타입 안전성을 보장해주진 않는다.
KProperty
의 call
메소드를 호출할 수도 있다. KProperty
의 call
은 프로퍼티의 게터를 호출한다. 하지만 프로퍼티 인터페이스는 프로퍼티 값을 얻는 더 좋은 방법으로 get
메소드를 제공한다.
var counter = 0
>>> val kProperty = ::counter
>>> kProperty.setter.call(21) // 리플렉션 기능을 통해 세터를 호출하면서 21을 인자로 넘긴다.
>>> println(kProperty.get()) // get을 호출해 프로퍼티 값을 가져온다.
21
멤버 프로퍼티는 KProperty1
인스턴스로 표현된다. 그 안에는 인자가 1개인 get
메소드가 들어있다. 멤버 프로퍼티는 어떤 객체에 속해 있는 프로퍼티이므로 멤버 프로퍼티의 값을 가져오려면 get
메소드에게 프로퍼티를 얻고자 하는 객체 인스턴스를 넘겨야 한다.
class Person(val name: String, val age: Int)
>>> val person = Person("Alice", 29)
>>> val memberProperty = Person::age // memberProperty 변수에 프로퍼티 참조를 저장한 다음에
>>> println(memberProperty.get(person)) // person 인스턴스의 프로퍼티 값을 가져온다. 따라서 memberProperty가 Person 클래스의 age 프로퍼티를 참조한다면 memberProperty.get(person)은 동적으로 person.age를 가져온다.
29
KProperty1
은 제네릭 클래스다. memberProperty
변수는 KProperty<Person, Int>
타입으로, 첫 번째 타입 파라미터는 수신 객체 타입, 두 번째 타입 파라미터는 프로퍼티 타입을 표현한다. 따라서 수신 객체를 넘길 때는 KProperty1
타입 파라미터와 일치하는 타입의 객체만을 넘길 수 있고 memberProperty.get("Alice")
는 컴파일되지 않는다.
최상위 수준이나 클래스 안에 정의된 프로퍼티만 리플렉션으로 접근할 수 있고 함수의 로컬 변수에는 접근할 수 없다는 점을 알아둬야 한다.
KClass
는 클래스와 객체를 표현할 때 쓰인다.KProperty
는 모든 프로퍼티를 표현할 수 있고, 그 하위 클래스인 KMutableProperty
는 var로 정의한 변경 가능한 프로퍼티를 표현한다.KProperty
와 KMutableProperty
에 선언된 Getter
와 Setter
인터페이스로 프로퍼티 접근자를 함수처럼 다룰 수 있다.
10.2.2 리플렉션을 사용한 객체 직렬화 구현
제이키드의 직렬화 함수 선언.
fun serialize(obj: Any): String = buildString { serializeObject(obj) }
buildString
은 StringBuilder
를 생성해서 인자로 받은 람다에 넘긴다. 람다 본문에서 serializeObject(obj)
를 호출해서 obj를 직렬화한 결과를 StringBuilder
에 추가한다.
아래 직렬화 함수는 객체를 받아서 그 객체에 대한 JSON 표현을 문자열로 돌려준다.
private fun StringBuilder.serializeObject(obj: Any) {
val kClass = obj.javaClass.kotlin // 객체의 KClass를 얻는다.
val properties = kClass.memberProperties // 클래스의 모든 프로퍼티를 얻는다.
properties.joinToStringBuilder(this, prefix = "{", postfix = "}") { prop -> // prop 변수의 타입은 KProperty1<Any, *> 이며, prop.get(obj) 메소드 호출은 Any 타입의 값을 반환한다.
serializeString(prop.name) // 프로퍼티 이름을 얻는다.
append(": ")
serializePropertyValue(prop.get(obj)) // 프로퍼티 값을 얻는다.
}
}
10.2.3 애노테이션을 활용한 직렬화 제어
- @JsonExclude :
KAnnotatedElement.findAnnotation
을 사용하여 구현.findAnnotation
은 인자로 전달받은 타입에 해당하는 애노테이션이 있으면 그 애노테이션을 반환한다. 아래와 같이findAnnotation
을filter
와 함께 사용하면@JsonExclude
로 애노테이션된 프로퍼티를 없앨 수 있다.
var properties = kClass.memberProperties
.filter { it.findAnnotation<JsonExclude>() == null }
- @JsonName : 이 경우에는 애노테이션의 존재 여부뿐 아니라 애노테이션에 전달한 인자도 알아야 한다.
@JsonName
의 인자는 프로퍼티를 직렬화해서 JSON에 넣을 때 사용할 이름이다.findAnnotation
을 이용해서 구현한다. val jsonNameAnn = prop.findAnnotation<JsonName>() // @JsonName 애노테이션이 있으면 그 인스턴스를 얻는다. val propName = jsonNameAnn?.name ?: prop.name // 애노테이션에서 "name"인자를 찾고 그런 인자가 없으면 "prop.name"를 사용한다.
- @CustomSerializer :
getSerializer
라는 함수에 기초하여 구현한다.getSerializer
는@CustomSerializer
를 통해 등록한ValueSerializer
인스턴스를 반환한다. 아래 코드는getSerializer
구현.
fun KProperty<*>.getSerializer(): ValueSerializer<Any?>? { val customSerializerAnn = findAnnotation<CustomSerializer>() ?: return null // findAnnotation를 호출해서 @CustomSerializer 애노테이션이 있는지 찾는다. val serializerClass = customSerializerAnn.serializerClass // @CustomSerializer 애노테이션이 있다면 그 애노테이션의 serializerClass가 직렬화기 인스턴스를 얻기 위해 사용해야 할 클래스다. val valueSerializer = serializerClass.objectInstance ?: serializerClass.createInstance() @Suppress("UNCHECKED_CAST") return valueSerializer as ValueSerializer<Any?> }
아래 코드는 위 getSerializer
를 사용한 serializeProperty
의 최종버전.
private fun StringBuilder.serializeProperty(
prop: KProperty1<Any, *>, obj: Any
) {
val jsonNameAnn = prop.findAnnotation<JsonName>()
val propName = jsonNameAnn?.name ?: prop.name
serializeString(propName)
append(": ")
val value = prop.get(obj)
val jsonValue = prop.getSerializer()?.toJsonValue(value) ?: value // 프로퍼티에 대해 정의된 커스텀 직렬화기가 있으면 그 커스텀 직렬화기를 사용하고, 없으면 일반적인 방법을 따라 프로퍼티를 직렬화한다.
serializePropertyValue(jsonValue)
}
10.2.4 JSON 파싱과 객체 역직렬화/10.2.5 최종 역직렬화 단계: callBy(), 리플렉션을 사용해 객체 만들기
역직렬화 API는 직렬화와 마찬가지로 함수 하나로 이뤄져 있다. 역직렬화할 객체의 타입을 실체화한 타입 파라미터로 deserialize
함수에 넘겨서 새로운 객체 인스턴스를 얻는다.
inline fun <reified T: Any> deserialize(json: String): T
data class Author(val name: String)
data class Book(val title: String, val author: Author)
>>> val json = """{"title": "Catch-22", "author": {"name": "J. Heller"}}"""
>>> println(deserialize<Book>(json))
Book(title=Catch-22, author=Author(name=J. Heller))
JSON 역직렬화 과정은 렉서(어휘분석기)/파서(문법분석기)/역직렬화 컴포넌트(파싱한 결과로 객체를 생성) 3단계로 분류.
- 렉서는 여러 문자로 이뤄진 입력 문자열을 토큰의 리스트로 변환한다. 토큰에는 문자 토큰(콤마,콜론,중괄호,각괄호), 값 토큰(문자열,수,불리언값) 2가지 종류가 있다.
- 파서는 토큰의 리스트를 구조화된 표현으로 반환한다. 제이키드에서 파서는 JSON의 상위 구조를 이해하고 토큰을 JSON에서 지원하는 의미 단위로 변환하는 일을 한다. 그런 의미 단위로는 키/값 쌍과 배열이 있다. JsonObject 인터페이스는 현재 역직렬화하는 중인 객체나 배열을 추적한다. 파서는 현재 객체의 새로운 프로퍼티를 발견할 때마다 그 프로퍼티의 유형(값, 복합 프로퍼티, 배열)에 해당하는 JsonObject의 함수를 호출한다.
// JSON 파서 콜백 인터페이스
interface JsonObject {
fun setSimpleProperty(propertyName: String, value: Any?)
fun createObject(propertyName: String): JsonObject
fun createArray(propertyName: String): JsonObject
}
각 메소드의 propertyName 파라미터는 JSON 키를 받는다. 따라서 파서가 객체를 값으로 하는 author
프로퍼티를 만나면 createObject("author")
메소드가 호출된다. 간단한 프로퍼티 값은 setSimpleProperty
를 호출하면서 실제 값을 value
에 넘기는 방식으로 등록한다.
- 역직렬화기는
JsonObject
에 상응하는 코틀린 타입의 인스턴스를 점차 만들어내는JsonObject
구현을 제공한다. 클래스 프로퍼티와 JSON 키 사이의 대응 관계를 찾아내고 중첩된 객체 값(Author의 인스턴스)을 만들어 낸다. 모든 중첩 객체를 만들고 난 뒤에는 필요한 클래스(Book)의 인스턴스를 새로 만든다.
'프로그래밍 > 코틀린' 카테고리의 다른 글
11장. DSL 만들기 (0) | 2022.01.07 |
---|---|
9장. 제네릭스 (2) | 2021.12.20 |
8장. 고차 함수: 파라미터와 반환 값으로 람다 사용 (0) | 2021.12.18 |
7장. 연산자 오버로딩과 기타 관례 (0) | 2021.12.14 |
6장. 코틀린 타입 시스템 (0) | 2021.12.13 |
댓글