Kotlin

[Kotlin] 리플렉션(Reflection) -2 (애노테이션을 리플렉션으로 처리 feat.JKid)

sh1mj1 2024. 4. 11. 15:02

이전 글에서 이어진다.

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

JKid 의 직렬화 함수는 아래와 같다.

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

 객체를 받아서 그 객체에 대한 JSON 표현을 문자열로 돌려준다.

객체의 프로퍼티와 값을 직렬화하면서 StringBuilder객체 뒤에 직렬화한 문자열을 추가한다.

이를 편하게 하기 위해 buildString을 사용했다.

kotlin
닫기
@kotlin.internal.InlineOnly
public inline fun buildString(builderAction: StringBuilder.() -> Unit): String =
    StringBuilder().apply(builderAction).toString()

 이렇게 함수 파라미터를 확장 함수의 수신 객체로(StringBuilder의 수신 객체로) 바꾸는 방식은 코틀린 코드에서 흔히 사용하는 패턴이다.

 

serialize는 대부분의 작업을 serializeObject에 위임하고 있다.

serializeObject(obj)를 호출해서 obj를 직렬화한 결과를 StringBuilder에 추가한다.

직렬화 함수 serializeObject

기본적으로 직렬화함수는 객체의 모든 프로퍼티를 직렬화한다.

아래 serializeObject를 조금 더 설명하기 쉽게, 몇가지 기능을 빼고 풀어쓴 코드이다.

kotlin
닫기
private fun StringBuilder.serializeObject(obj: Any) {
    val kClass = obj.javaClass.kotlin // 객체의 KClass 를 얻는다
    val properties = kClass.memberProperties // 클래스의 모든 프로퍼티를 얻는다
    properties.joinToStringBuilder(
        this, prefix = "{", postfix = "}") { prop ->
            serializeString(prop.name) // 프로퍼티 이름을 얻는다
            append(": ")
            serializePropertyValue(prop.get(obj)) // 프로퍼티 값을 얻는다
        }
}

 클래스의 각 프로퍼티를 차례대로 직렬화하여 결과 JSON 은 { prop1: value1, prop2: value2 } 같은 형태가 된다.

  • joinToStringBuilder: 프로퍼티를 콤마(,)로 분리함.
  • serializeString: JSON 명세에 따라 특수 문자를 이스케이프함.
  • serializePropertyValue: 어떤 값이 원시 타입, 문자열, 컬렉션, 중첩된 객체 중 어떤 것인지 판단하고 그에 따라 적절히 직렬화함.

이전 글에서는 KProperty 인스턴스의 값을 얻을 때 get 메서드를 사용할 수 있다고 했다.

이 때 예제에서는 KProperty1<Person, Int> 타입인 Person::age를 처리했기 때문에 컴파일러가 수신 객체와 프로퍼티 값의 타입을 정확히 알 수 있었다.

 

하지만 이 직렬화 예제에서는 어떤 객체의 클래스에 정의된 모든 프로퍼티를 열거한다.

그래서 각 프로퍼티가 정확히 어떤 타입인지 알 수 없다.

따라서 prop변수의 타입은 KProperty1<Any, *>가 된다. 

그리고 prop.get(obj)메서드 호출은 Any타입의 값을 리턴한다. (*였기 때문)

이러한 경우, 수신 객체 타입을 컴파일 타임에 검사할 방법이 없다. 

하지만 get 에 넘기는 객체가 프로퍼티로부터 얻어온 obj 이기 때문에 항상 프로퍼티 값은 제대로 리턴된다.

이럴 때 리플렉션을 사용하면 유용하다!!

애노테이션을 활용한 직렬화 제어

이제 JKid 의 @JsonExclude, @JsonName, @CustomSerializer 애노테이션을 serializeObject 함수가 어떻게 처리하는지 살펴보자.

@JsonExclude

어떤 프로퍼티를 직렬화에서 제외하고 싶을 때 사용한다고 했다.

클래스의 모든 멤버 프로퍼티를 가져올 때 KClass 인스턴스.memberProperties 를 사용했었다.

그런데 우리는 @JsonExclude 애노테이션이 붙은 프로퍼티는 제외해야 한다.

 

KAnnotatedElement 인터페이스에는 annotations 라는 프로퍼티가 있다.

 annotations 은 소스코드 상에서 해당 요소에 적용된 모든 애노테이션 인스턴스의 리스트이다.

물론 이 애노테이션들은 @RententionRUNTIME 으로 지정한 것들이다.

KPropertyKAnnotatedElement 를 상속받는다.

그래서 property.annotations 를 통해서 프로퍼티의 모든 애노테이션을 얻을 수 있다.

 

우리는 모든 애노테이션이 필요한 것이 아니라, @JsonExclude 라는 애노테이션만 찾으면 된다.

findAnnotation 이라는 확장 KAnnotatedElement 에 대한 확장 함수를 사용하면 된다.

그리고 findAnnotation 함수를 filter 와 함께 사용하면 @JsonExclude 로 애노테이션된 프로퍼티를 없앨 수 있다.

kotlin
닫기
val properteis = kClass.memberProperties
        .filter { it.findAnnotation<JsonExclude>() == null }

@JsonName

kotlin
닫기
annotation class JsonName(val name: String)

data class Person(
    @JsonName("alias") val firstName: String,
    val age: int
}

 @JsonName의 경우 애노테이션의 존재 여부 뿐 아니라 애노테이션에 전달한 인자도 알아야 한다.

인자는 프로퍼티를 직렬화 해서 JSON 에 넣을 때 사용할 이름이다.

 

이 경우에도 findAnnotation 을 사용할 수 있다.

kotlin
닫기
val jsonNameAnn = prop.findAnnotation<JsonName>() // @JsonName 애노테이션이 있으면 그 인스턴스를 얻는다
val propName = jsonNameAnn?.name ?: prop.name // 애노테이션에서 "name" 인자를 찾고 그런 인자가 없으면 prop.name 을 사용한다

 프로퍼티에 @JsonName 애노테이션이 없다면 jsonNamenull 이 된다.

그 경우에는 기존 프로퍼티의 이름을 사용하여 직렬화해야 한다.

 

이렇게 @JsonExclude@JsonName 애노테이션을 적용하여 직렬화 로직을 변경해보자.

kotlin
닫기
private fun StringBuilder.serializeObject(obj: Any) {
    obj.javaClass.kotlin.memberProperties
        .filter { it.findAnnotation<JsonExclude>() == null }
        .joinToStringBuilder(this, prefix = "{", postfix = "}") {
            serializeProperty(it, obj)
        }
}

private fun StringBuilder.serializeProperty(prop: KProperty1<Any, *>, obj: Any) {
    val jsonName = prop.findAnnotation<JsonName>()
    val propName = jsonName?.name ?: prop.name
    serializeString(propName)
    append(": ")
    serializePropertyValue(prop.get(obj))
}

 serializeStringserializePropertyValue 메서드의 구현은 지금 설명하는 것의 핵심과 멀기 때문에 따로 적지 않겠다.

@CustomSerializer

나머지 애노테이션이다. 이 애노테이션에 대한 정보는 아래와 같다.

kotlin
닫기
interface ValueSerializer<T> {
    fun toJsonValue(value: T): Any?
    fun fromJsonValue(jsonValue: Any?): T
}

@Target(AnnotationTarget.PROPERTY)
annotation class CustomSerializer(val serializerClass: KClass<out ValueSerializer<*>>)

 @CustomSerializer 는 아래 getSerializer 라는 함수에 기초한다.

kotlin
닫기
fun KProperty<*>.getSerializer(): ValueSerializer<Any?>? {
    val customSerializerAnn = findAnnotation<CustomSerializer>() ?: return null
    val serializerClass = customSerializerAnn.serializerClass

    val valueSerializer = serializerClass.objectInstance?: serializerClass.createInstance()
    @Suppress("UNCHECKED_CAST")
    return valueSerializer as ValueSerializer<Any?>
}

 getSerializer@CustomSerializer를 통해 등록한 ValueSerializer 인스턴스를 리턴한다.

  • findAnnotation 함수로 @CustomSerializer 애노테이션이 있는지 찾는다.
  • @CustomSerializer가 있다면 이를 통해 serializer 클래스 인스턴스를 얻고 이를 리턴한다.

예를 들어서

kotlin
닫기
data class Person(
    val name: String,
    @CustomSerializer(DateSerializer::class) val birthDate: Date
)

 위처럼 birthDate 프로퍼티를 직렬화하면서 getSerializer()를 호출하면 DateSerializer 인스턴스를 얻을 수 있다.

 

그런데 getSerializer 함수에서 이 코드가 눈에 띈다.

kotlin
닫기
val valueSerializer = serializerClass.objectInstance ?: serializerClass.createInstance()

 클래스와 싱글턴 객체(object) 는 모두 KClass 클래스로 표현된다.

하지만 싱글턴 객체에서는 object 선언에 의해 생성된 싱글턴을 가리키는 objectInstance라는 프로퍼티가 있다.

만약 클래스가 아닌, 싱글턴 객체로 생성했다면, 싱글턴 인스턴스로 모든 객체를 직렬화하면 되기 때문에 createInstance를 호출할 필요가 없다.

 

그렇다면 이제 마지막으로 serializeProperty 를 발전시켜보자.

serializerProperty 구현 안에서 getSerializer를 사용할 수 있다.

kotlin
닫기
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)
    // 프로퍼티에 정의된 @CUstomSerializer 가 있다면 그 serializer를 사용한다
    // 만약 없다면 일반적인 방법에 따라 프로퍼티를 직렬화한다
    val jsonValue = prop.getSerializer()?.toJsonValue(value) ?: value
    serializePropertyValue(jsonValue)
}

 이렇게 JKid 라이브러리의 직렬화 부분을 모두 살펴보았다.

JSON 파싱과 객체 역직렬화

JKid 역직렬화 API는 직렬화와 마찬가지로 함수 하나로 이루어져 있다.

kotlin
닫기
inline fun <reified T: Any> deserialize(json: String): T {
    return deserialize(StringReader(json))
}

 이 코드는 아래처럼 사용하면 된다.

data class Author(val fullName: String)
data class Book(val name: String, val authors: List<Author>)
kotlin
닫기
val json = """{"title": "Catch-22", "atuhor": {"name": "J.Heller"}}"""
val book = deserialize<Book>(json)

println(book)
// Book(title=Catch-22, author=Author(name=J.Heller)) 출력

 역직렬화할 객체의 타입을 실체화한(reified) 타입 파라미터로 deserialize 함수에 넘겨서 새로운 객체 인스턴스를 얻는다.

역직렬화 과정

역직렬화는 아래 과정을 거쳐야 한다.

  • JSON 문자열 입력을 파싱
  • 리플렉션을 사용해 객체의 내부에 접근
  • -> 새로운 객체와 프로퍼티를 생성

그래서 역직렬화가 직렬화보다 더 어렵다.

JKide 의 JSON 역직렬화는 흔히 쓰는 다른 방법과 똑같이 3 단계로 구현되어 있다.

  • lexer(Lexical Analyzer, 어휘 분석기)
  • paser(Syntax Analyzer, 문법 분석기)
  • 역직렬화 컴포넌트(파싱한 결과로 객체를 생성)

Lexer 는 여러 문자로 이뤄진 입력 문자열을 token 의 리스트로 변환한다.

토큰은 문자 토큰( , : { } [ ] ) 과 값 토큰( 문자열, 수, boolean, null 상수) 로 나뉜다.

 

Paser 는 token 리스트를 구조화된 표현으로 변환한다.

JSON 의 상위 구조를 이해하고, 토큰을 JSON 에서 지원하는 의미 단위(키/값 쌍, 배열)로 변환한다.

JsonObject

JsonObject 인터페이스는 현재 역직렬화하는 중인 객체/배열을 추적한다.

Parser는 현재 객체의 새로운 프로퍼티를 발견할 때마다 그 프로퍼티의 타입에 해당하는 JsonObject 의 함수를 호출한다.

interface JsonObject {
    fun setSimpleProperty(propertyName: String, value: Any?)

    fun createObject(propertyName: String): JsonObject

    fun createArray(propertyName: String): JsonObject
}

 각 메서드의 인자 propertyName 파라미터는 JSON 키를 받는다.

Parser 가 객체를 값으로 하는 author 프로퍼티를 만난다면, createObject("author") 메서드가 호출된다.

간단한 프로퍼티 값을 만나면 setSimpleProperty 를 호출한다.

 

즉, JsonObject 를 구현하는 클래스는 새로운 객체를 생성하고 새로 생성한 객체를 외부 객체에 등록하는 과정을 책임져야 한다.

JSON 파싱: Lexer, Parser, Deserializer

 참고로 JKid 는 데이터 클래스와 함께 사용하려는 의도로 만든 라이브러리이다.

객체를 생성한 후에 프로퍼티를 생성하는 기능을 지원하지 않는다.

Seed

이렇게 JKid 에서 역직렬화 시, 그 객체의 하위 요소를 저장해야 한다는 것은 전통적인 Builder 패턴과 비슷하다.

여기서는 Seed라는 이름을 사용한다.

JSON 에서는 객체, 컬렉션, 맵과 같은 복합 구조를 만들 필요가 있다.

  • ObjectSeed: 객체를 만드는,
  • ObjectListSeed: 복합 객체로 이루어진 리스트를 만드는,
  • ValueListSeed: 간단한 값으로 이루어진 리스트를 만드는 일을 한다.

추가로 맵을 만드는 Seed 를 구현할 수도 있을 것이다.

 

JsonObjct 를 확장하는 기본 Seed 인터페이스

interface Seed: JsonObject {
    val classInfoCache: ClassInfoCache

    fun spawn(): Any? // 객체 생성 과정이 끝난 후 결과 인스턴스를 얻기 위한 spawn 메서드를 추가 제공

    // 중첩된 객체/리스트를 만들 때 사용
    fun createCompositeProperty(propertyName: String, isList: Boolean): JsonObject

    override fun createObject(propertyName: String) = createCompositeProperty(propertyName, false)

    override fun createArray(propertyName: String) = createCompositeProperty(propertyName, true)
}

 spawn을 Builder 패턴의 build 와 비슷하다고 생각할 수 있다. 둘 다 만들어낸 객체를 돌려주는 메서드이다.

spawnObjectSeed 에 대해서는 생성된 객체를, ObjectListSeedValueListSeed 의 경우 생성된 리스트를 리턴한다.

deserialize 함수

값을 역직렬화하는 모든 과정을 처리하는 deserialize 함수이다.

fun <T: Any> deserialize(json: Reader, targetClass: KClass<T>): T {
    val seed = ObjectSeed(targetClass, ClassInfoCache()) // 시드 생성
    Parser(json, seed).parse() // 입력 스트림 Reader 인 json과 시드를 인자로 전달
    return seed.spawn() // 결과 객체 생성
}

위에서 만들고 있는 ObjectSeed 의 구현을 보자.

ObjectSeed 

class ObjectSeed<out T: Any>(targetClass: KClass<T>, override val classInfoCache: ClassInfoCache) : Seed {
    // targetClass 의 인스턴스를 만들 때 필요한 정보를 캐시
    private val classInfo: ClassInfo<T> = classInfoCache[targetClass]

    private val valueArguments = mutableMapOf<KParameter, Any?>()
    private val seedArguments = mutableMapOf<KParameter, Seed>()

    // 생성자 파라미터와 그 값을 연결하는 맵을 만듦
    private val arguments: Map<KParameter, Any?>
        get() = valueArguments + seedArguments.mapValues { it.value.spawn() }

    override fun setSimpleProperty(propertyName: String, value: Any?) {
        val param = classInfo.getConstructorParameter(propertyName)
        // null생성자 파라미터 값이 간단한 값인 경우 그 값을 기록한다
        valueArguments[param] = classInfo.deserializeConstructorArgument(param, value)
    }

    override fun createCompositeProperty(propertyName: String, isList: Boolean): Seed {
        val param = classInfo.getConstructorParameter(propertyName) 
        // 프로퍼티에 대한 DeserializeInterface 애노테이션이 있다면 그 값을 가져온다
        val deserializeAs = classInfo.getDeserializeClass(propertyName)
        
        // 파라미터 타입에 따라 ObjectSeed 나 CollectionSeed 를 만든다
        val seed = createSeedForType(deserializeAs ?: param.type.javaType, isList)
        
        // 바로 위에서 만든 시드 객체를 seedArgument 탭에 기록한다
        return seed.apply { seedArguments[param] = this } // 바로 위에서 
    }

    // 인자 맵을 넘겨서 targetClass 타입의 인스턴스를 만든다
    override fun spawn(): T = classInfo.createInstance(arguments)
}

 ObjectSeed 는 결과 클래스에 대한 참조(targetClass: KClass<T>)와 결과 클래스의 프로퍼티 정보를 저장하는 캐시인 classInfoCache 객체를 인자로 받는다.

나중에 이 캐시 정보를 사용해서 클래스 참조 인스턴스를 만든다.

  • ObjectSeed 는 생성자 파라미터와 값을 연결해주는 맵(arguments)을 만든다. 이를 위해서 두 가지의 mutableMap 을 만든다.
    • valueArguments 는 간단한 값 프로퍼티를 저장한다.
    • seedArguments 는 복합 프로퍼티를 저장한다.
  • 결과를 만들면서 setSimpleProperty 를 호출해서 valueArguments 맵에 새 인자를 추가한다.
  • createCompositeProperty 를 호출해서 seedArguments 맵에 새 인자를 추가한다.
  • 초기 상태에서 새로운 복합 시드를 추가한 후 입력 스트림에서 들어오는 데이터로 그 복합 시드에 데이터를 채워 넣는다.
  • 마지막으로 spawn 메서드는 내부에 중첩된 모든 시드의 spawn 을 재귀적으로 호출해서 내부 객체 계층 구조를 만든다.

여기서 spawn 메서드 본문의 arguments 가 재귀적으로 복합 시드를 만든다.

arguments 프로퍼티의 custom getter 안에서는 mapValues 메서드를 사용해서 seedArguments 의 각 원소에 대해 spawn 메서드를 호출한다.

createSeedForType 함수는 파라미터의 타입을 분석해서 적절히 Seed 를 생성해주는 함수이다.

최종 역직렬화 단계: callBy(), 리플렉션을 사용해 객체 만들기

이제 최종 결과인 객체 인스턴스를 생성하고 생성자 파라미터를 정보를 캐싱하는 ClassInfo 클래스를 이해하면 된다.

이는 ObjectSeed 안에서 쓰인다.

 

그 이전에 먼저 리플렉션을 통해 객체를 만들 때 사용할 API 를 몇가지 살펴보자.

리플렉션을 통해 객체를 만드는 API

KCallable.call

KCallable.call 은 인자 리스트를 받아서 함수나 생성자를 호출해준다고 했다. 이는 디폴트 파라미터 값을 지원하지 않는다.

만약 역직렬화 시 생성해야 하는 객체에 디폴트 생성자 파라미터 값이 있을 때, 그 디폴트 값을 활용할 수 있다면 JSON 에서 관련 프로퍼티를 꼭 지정하지 않아도 된다.

KCallable.callBy 는 디폴트 파라미터를 지원한다.

kotlin
닫기
// Represents a callable entity, such as a function or a property.
public actual interface KCallable<out R> : KAnnotatedElement {
    // ...
    
    /* 
    Calls this callable with the specified mapping of parameters to arguments and returns the result.
    If a parameter is not found in the mapping and is not optional (as per KParameter.isOptional),
    or its type does not match the type of the provided value, an exception is thrown.
    */
    public fun callBy(args: Map<KParameter, Any?>): R
}

 이 메서드는 파라미터와 파라미터에 해당하는 값을 연결해주는 맵을 인자로 받는다.

만약 인자로 받은 맵(args) 에서 파라미터를 찾을 수 없고 파라미터 디폴트 값이 정의되어 있으면 그 디폴트 값을 사용한다.

그리고 이것을 사용하면, 파라미터의 순서에 상관 없이, 이름/값 쌍을 읽어서 이름과 일치하는 파라미터를 찾은 후 맵에 파라미터 정보와 값을 넣을 수 있다.

 

args 맵에 들어있는 각 값의 타입이 생성자의 파라미터 타입과 일치해야 한다.

특히 숫자 타입에서 Int, Long, Double 등의 타입 중 어떤 것인지를 확인해서 JSON 에 있는 숫자 값을 적절한 타입으로 변환해야 한다.

KParameter.type 프로퍼티를 활용하면 파라미터의 타입을 알 수 있다.

타입 변환에는 CustomSerializer 에 사용했던 ValueSerializer 인스턴스를 똑같이 사용한다.

fun serializerForType(type: Type): ValueSerializer<out Any?>? =
        when (type) {
            Byte::class.java, Byte::class.javaObjectType -> ByteSerializer
            // ....
            String::class.java -> StringSerializer
            else -> null
        }

 각 ~~Serializer 는 싱글턴 object 로 구현되어 있다.

예를 하나 들어보면, 

object IntSerializer : ValueSerializer<Int> {
    override fun fromJsonValue(jsonValue: Any?) = jsonValue.expectNumber().toInt()
    override fun toJsonValue(value: Int) = value
}

private fun Any?.expectNumber(): Number {
    if (this !is Number) throw JKidException("Expected number, was: $this")
    return this
}

 callBy 메서드에 생성자 파라미터와 그 값을 연결해 주는 맵을 넘기면 객체의 생성자를 호출 할 수 있다.

ValueSerializer 매커니즘을 사용해 생성 자를 호출 할 때 사용하는 맵에 들어가는 값이 생성자 파라미터 정의 의 타입과 일치하게 만든다.

ClassInfoCache

직렬화/역직렬화에 사용하는 애노테이션들(@JsonName, @CustomSerializer)은 프로퍼티에 적용된다.

하지만 객체를 역직렬화할 때는 프로퍼티가 아니라, 생성자 파라미터를 다루어야 한다.

따라서 애노테이션을 꺼내려면 파라미터에 해당하는 프로퍼티를 찾아야 한다.

JSON 에서 모든 키/값을 읅일 때마다 이런 검색을 수행하면 코드가 느려질 수 있다.

그래서 클래스 별로 한 번만 검색을 수행하고 검색 결과를 캐시에 넣어둔다.

ClassInfoCache 가 그 역할을 해주는 리플렉션 연산의 비용을 줄이기 위한 클래스이다.

class ClassInfoCache {
    private val cacheData = mutableMapOf<KClass<*>, ClassInfo<*>>()

    @Suppress("UNCHECKED_CAST")
    operator fun <T : Any> get(cls: KClass<T>): ClassInfo<T> =
            cacheData.getOrPut(cls) { ClassInfo(cls) } as ClassInfo<T>
}

 맵에 값을 저장할 때는 타입 정보가 사라진다.

하지만 get 메서드가, 리턴된 ClassInfo 가 올바른 타입 인자를 가지고 있음을 보장한다.

getOrPut 을 통해 cls 에 대한 entry 가 맵에 있다면 리턴, 없다면 저장해준 후에 리턴한다.

ClassInfo

ClassInfo 클래스는 대상 클래스의 새 인스턴스를 만들고 필요한 정보를 캐시해둔다.

class ClassInfo<T : Any>(cls: KClass<T>) {
    private val className = cls.qualifiedName
    private val constructor = cls.primaryConstructor
            ?: throw JKidException("Class ${cls.qualifiedName} doesn't have a primary constructor")

    private val jsonNameToParamMap = hashMapOf<String, KParameter>()
    private val paramToSerializerMap = hashMapOf<KParameter, ValueSerializer<out Any?>>()
    private val jsonNameToDeserializeClassMap = hashMapOf<String, Class<out Any>?>()

    init {
        constructor.parameters.forEach { cacheDataForParameter(cls, it) }
    }

    private fun cacheDataForParameter(cls: KClass<*>, param: KParameter) {
        val paramName = param.name
                ?: throw JKidException("Class $className has constructor parameter without name")

        val property = cls.declaredMemberProperties.find { it.name == paramName } ?: return
        val name = property.findAnnotation<JsonName>()?.name ?: paramName
        jsonNameToParamMap[name] = param

        val deserializeClass = property.findAnnotation<DeserializeInterface>()?.targetClass?.java
        jsonNameToDeserializeClassMap[name] = deserializeClass

        val valueSerializer = property.getSerializer()
                ?: serializerForType(param.type.javaType)
                ?: return
        paramToSerializerMap[param] = valueSerializer
    }

    fun getConstructorParameter(propertyName: String): KParameter = jsonNameToParamMap[propertyName]
            ?: throw JKidException("Constructor parameter $propertyName is not found for class $className")

    fun getDeserializeClass(propertyName: String) = jsonNameToDeserializeClassMap[propertyName]

    fun deserializeConstructorArgument(param: KParameter, value: Any?): Any? {
        val serializer = paramToSerializerMap[param]
        if (serializer != null) return serializer.fromJsonValue(value)

        validateArgumentType(param, value)
        return value
    }

    private fun validateArgumentType(param: KParameter, value: Any?) {
        if (value == null && !param.type.isMarkedNullable) {
            throw JKidException("Received null value for non-null parameter ${param.name}")
        }
        if (value != null && value.javaClass != param.type.javaType) {
            throw JKidException("Type mismatch for parameter ${param.name}: " +
                    "expected ${param.type.javaType}, found ${value.javaClass}")
        }
    }

    fun createInstance(arguments: Map<KParameter, Any?>): T {
        ensureAllParametersPresent(arguments)
        return constructor.callBy(arguments)
    }

    private fun ensureAllParametersPresent(arguments: Map<KParameter, Any?>) {
        for (param in constructor.parameters) {
            if (arguments[param] == null && !param.isOptional && !param.type.isMarkedNullable) {
                throw JKidException("Missing value for parameter ${param.name}")
            }
        }
    }
}

 초기화 시에(init 에서) 각 생성자 파라미터에 해당하는 프로퍼티를 찾아서 애노테이션을 가져온다.

데이터는 세 가지 맵에 저장한다.

  • jsonNameToParamMap: JSON 파일의 각 키에 해당하는 파라미터를 저장
  • paramToSerializerMap: 각 파라미터에 대한 직렬화기를 저장
  • jsonNameToDeserializeClassMap: @DeserializeInterface 애노테이션 인자로 지정한 클래스를 저장.

ClassInfo 는 프로퍼티 이름으로 생성자 파라미터를 제공할 수 있다.

생성자 호출 코드는 파라미터-아규먼트 map 의 키로 생성자 파라미터를 사용한다.

 

ensuerAllParametesPresent 함수는 생성자에 필요한 모든 필수 파라미터가 맵에 들어있는지 검사한다.

여기서 리플렉션 API 를 어떻게 활용하는지 살펴보자.

파라미터에 디폴트 값이 있다면 param.isOptional == true 이므로, 해당 파라미터에 대한 인자는 인자 맵에 없어도 된다.

파라미터가 nullable 값이라면 param.type.isMarkedNullable == true 이다. 이 때 디폴트 파라미터 값으로 null 을 사용한다.

만약 그 두 경우가 아닌데 파라미터에 대한 인자가 맵에 없다면, 예외를 발생시킨다.

 

이렇게 리플렉션 캐시를 사용하면 역직렬화 과정을 제어하는 애노테이션을 찾는 과정을 프로퍼티 이름 별로 단 한번만 수행할 수 있다.

JSON 데이터에서 발견한 모든 프로퍼티에 대해 반복할 필요가 없어진다.