본문 바로가기
프로그래밍/코틀린

10장. 애노테이션과 리플렉션

by 직장인 투자자 2022. 1. 3.

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를 제외한 모든 애트리뷰트에는 이름을 명시해야 한다.
  • 코틀린의 애노테이션 적용 문법은 일반적인 생성자 호출과 같다. 따라서 인자의 이름을 명시하기 위해 이름 붙인 인자 구문을 사용할 수도 있고 이름을 생략할 수도 있다. 여기서는 nameJsonName 생성자의 첫 번째 인자이므로 JsonName(name = "first_name")JsonName("first_name")과 같다.
  • 자바에서 선언한 애노테이션을 코틀린의 구성 요소에 적용할 때는 value를 제외한 모든 인자에 대해 이름 붙인 인자 구문을 사용해야만 한다.

10.1.5 메타애노테이션: 애노테이션을 처리하는 방법 제어

애노테이션 클래스에 적용할 수 있는 애노테이션을 메타애노테이션이라고 부른다. 표준 라이브러리에 있는 메타애노테이션 중 가장 흔히 쓰이는 메타애노테이션은 @Target이다. 제이키드의 JsonExcludeJsonName애노테이션도 적용가능 대상을 지정하기 위해 @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<*>>)

CustomSerializerValueSerializer를 구현하는 클래스만 인자로 받아야함을 명시할 필요가 있다. 예를 들어 DateValueSerializer를 구현하지 않으므로 @CustomSerializer(Date::class)라는 애노테이션을 금지시켜야 한다.

10 5

클래스를 인자로 받아야 하면 애노테이션 파라미터 타입에 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.

kFunctioninvoke 메소드를 호출할 때는 인자 개수나 타입이 맞아 떨어지지 않으면 컴파일이 안된다. 따라서 KFunction의 인자 타입과 반환 타입을 모두 다 안다면 invoke 메소드를 호출하는 게 낫다. call 메소드는 모든 타입의 함수에 적용할 수 있는 일반적인 메소드지만 타입 안전성을 보장해주진 않는다.

KPropertycall 메소드를 호출할 수도 있다. KPropertycall은 프로퍼티의 게터를 호출한다. 하지만 프로퍼티 인터페이스는 프로퍼티 값을 얻는 더 좋은 방법으로 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로 정의한 변경 가능한 프로퍼티를 표현한다.
KPropertyKMutableProperty에 선언된 GetterSetter 인터페이스로 프로퍼티 접근자를 함수처럼 다룰 수 있다.

10 6

10.2.2 리플렉션을 사용한 객체 직렬화 구현

제이키드의 직렬화 함수 선언.

fun serialize(obj: Any): String = buildString { serializeObject(obj) }

buildStringStringBuilder를 생성해서 인자로 받은 람다에 넘긴다. 람다 본문에서 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은 인자로 전달받은 타입에 해당하는 애노테이션이 있으면 그 애노테이션을 반환한다. 아래와 같이 findAnnotationfilter와 함께 사용하면 @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)의 인스턴스를 새로 만든다.

 

반응형

댓글