이전 글에서 이어진다.
- 이전 글
리플렉션을 사용한 객체 직렬화 구현
JKid 의 직렬화 함수는 아래와 같다.
fun serialize(obj: Any): String = buildString { serializeObject(obj) }
객체를 받아서 그 객체에 대한 JSON 표현을 문자열로 돌려준다.
객체의 프로퍼티와 값을 직렬화하면서 `StringBuilder`객체 뒤에 직렬화한 문자열을 추가한다.
이를 편하게 하기 위해 `buildString`을 사용했다.
@kotlin.internal.InlineOnly
public inline fun buildString(builderAction: StringBuilder.() -> Unit): String =
StringBuilder().apply(builderAction).toString()
이렇게 함수 파라미터를 확장 함수의 수신 객체로(`StringBuilder`의 수신 객체로) 바꾸는 방식은 코틀린 코드에서 흔히 사용하는 패턴이다.
`serialize`는 대부분의 작업을 `serializeObject`에 위임하고 있다.
`serializeObject(obj)`를 호출해서 `obj`를 직렬화한 결과를 `StringBuilder`에 추가한다.
직렬화 함수 serializeObject
기본적으로 직렬화함수는 객체의 모든 프로퍼티를 직렬화한다.
아래 `serializeObject`를 조금 더 설명하기 쉽게, 몇가지 기능을 빼고 풀어쓴 코드이다.
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` 은 소스코드 상에서 해당 요소에 적용된 모든 애노테이션 인스턴스의 리스트이다.
물론 이 애노테이션들은 `@Rentention` 을 `RUNTIME` 으로 지정한 것들이다.
`KProperty` 는 `KAnnotatedElement` 를 상속받는다.
그래서 `property.annotations` 를 통해서 프로퍼티의 모든 애노테이션을 얻을 수 있다.
우리는 모든 애노테이션이 필요한 것이 아니라, `@JsonExclude` 라는 애노테이션만 찾으면 된다.
`findAnnotation` 이라는 확장 `KAnnotatedElement` 에 대한 확장 함수를 사용하면 된다.
그리고 `findAnnotation` 함수를 `filter` 와 함께 사용하면 `@JsonExclude` 로 애노테이션된 프로퍼티를 없앨 수 있다.
val properteis = kClass.memberProperties
.filter { it.findAnnotation<JsonExclude>() == null }
@JsonName
annotation class JsonName(val name: String)
data class Person(
@JsonName("alias") val firstName: String,
val age: int
}
`@JsonName`의 경우 애노테이션의 존재 여부 뿐 아니라 애노테이션에 전달한 인자도 알아야 한다.
인자는 프로퍼티를 직렬화 해서 JSON 에 넣을 때 사용할 이름이다.
이 경우에도 `findAnnotation` 을 사용할 수 있다.
val jsonNameAnn = prop.findAnnotation<JsonName>() // @JsonName 애노테이션이 있으면 그 인스턴스를 얻는다
val propName = jsonNameAnn?.name ?: prop.name // 애노테이션에서 "name" 인자를 찾고 그런 인자가 없으면 prop.name 을 사용한다
프로퍼티에 `@JsonName` 애노테이션이 없다면 `jsonName` 이 `null` 이 된다.
그 경우에는 기존 프로퍼티의 이름을 사용하여 직렬화해야 한다.
이렇게 `@JsonExclude` 와 `@JsonName` 애노테이션을 적용하여 직렬화 로직을 변경해보자.
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))
}
`serializeString` 과 `serializePropertyValue` 메서드의 구현은 지금 설명하는 것의 핵심과 멀기 때문에 따로 적지 않겠다.
@CustomSerializer
나머지 애노테이션이다. 이 애노테이션에 대한 정보는 아래와 같다.
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` 라는 함수에 기초한다.
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` 클래스 인스턴스를 얻고 이를 리턴한다.
예를 들어서
data class Person(
val name: String,
@CustomSerializer(DateSerializer::class) val birthDate: Date
)
위처럼 `birthDate` 프로퍼티를 직렬화하면서 `getSerializer()`를 호출하면 `DateSerializer` 인스턴스를 얻을 수 있다.
그런데 `getSerializer` 함수에서 이 코드가 눈에 띈다.
val valueSerializer = serializerClass.objectInstance ?: serializerClass.createInstance()
클래스와 싱글턴 객체(object) 는 모두 `KClass` 클래스로 표현된다.
하지만 싱글턴 객체에서는 `object` 선언에 의해 생성된 싱글턴을 가리키는 `objectInstance`라는 프로퍼티가 있다.
만약 클래스가 아닌, 싱글턴 객체로 생성했다면, 싱글턴 인스턴스로 모든 객체를 직렬화하면 되기 때문에 `createInstance`를 호출할 필요가 없다.
그렇다면 이제 마지막으로 `serializeProperty` 를 발전시켜보자.
`serializerProperty` 구현 안에서 `getSerializer`를 사용할 수 있다.
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는 직렬화와 마찬가지로 함수 하나로 이루어져 있다.
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>)
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` 를 구현하는 클래스는 새로운 객체를 생성하고 새로 생성한 객체를 외부 객체에 등록하는 과정을 책임져야 한다.
참고로 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 와 비슷하다고 생각할 수 있다. 둘 다 만들어낸 객체를 돌려주는 메서드이다.
`spawn` 은 `ObjectSeed` 에 대해서는 생성된 객체를, `ObjectListSeed` 나 `ValueListSeed` 의 경우 생성된 리스트를 리턴한다.
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` 는 디폴트 파라미터를 지원한다.
// 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 데이터에서 발견한 모든 프로퍼티에 대해 반복할 필요가 없어진다.
'Kotlin' 카테고리의 다른 글
림시 flow (3) | 2024.10.30 |
---|---|
[Kotlin] 리플렉션(Reflection) -1 (KClass, KCallable, KFunction, KProperty) (0) | 2024.04.11 |
[Kotlin] 애노테이션 -2 (JKid 라이브러리를 통해 알아보기) (0) | 2024.04.10 |
[Kotlin] 애노테이션 - 1 (적용과 사용 지점 대상) (1) | 2024.04.10 |
[Kotlin] SAM 을 익명 객체 VS 람다로 구현 & fun interface (feat. 우테코) (1) | 2024.03.22 |