애노테이션 (Annotation)
아래 예는 JUnit Framework를 사용하려면, 테스트 메소드 앞에 @Test를 붙여야 한다.
import org.junit.*
class MyTest {
@Test fun testTrue() {
Assert.assertTrue(true)
}
}
애노테이션을 활용한 JSON직렬화 제어
# serialization
data class Person(val name: String, val age: Int)
>>> val person = Person("Alice", 29)
>>> println(serialize(person))
{"age": 29, "name": "Alice"}
# deserialization
>>> val json = """{"name": "Alice", "age": 29}"""
>>> println(deserialize<Person>(json))
Person(name=Alice, age=29)
애노테이션 선언
@JsonExclude 애노테이션은 아무 파라미터도 없는 가장 단순한 애노테이션이다. 애노테이션 선언은 일반 클래스 선언처럼 보인다. 일반 클래스와 차이는 class앞에 annotation이라는 변경자가 붙어있다는 점 뿐이다. 하지만 애노테이션은 오직 선언이나 식과 관련있는 메타데이터의 구조를 정의하기 때문에 내부에 아무 코드도 들어있을수 없다. 이런이유로 컴파일러가 막는다.
@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude
두번째로 만들 애노테이션은 파라미터가 있는 애노테이션이다. 파라미터는 애노테이션 클래스의 주생성자에 파라미터를 선언해야 한다. 다만 애노테이션 클래스는 모든 파리미터에 앞에 val을 붙여야 한다.
@Target(AnnotationTarget.PROPERTY)
annotation class JsonName(val name: String)
매타애노테이션 (애노테이션 클래서에 적용할수 있는 애노테이션)
표준라이브러리에서는 몇 가지 메타애노테이션이 있으면, 그런 애노테이션은 컴파일러가 애노테이션을 처리하는 방법을 제어한다. 가장흔히 쓰는 메타에노테이션은 @Taget이다. 제이키드 라이브러리는 프로퍼티 애노테이션만을 사용하므로, 애노테이션 클래스에 @Target을 지정해야 한다. 필요하다면 둘 이상의 대상을 한꺼번에 선언할수 있다. 만약 메타애노테이션을 직접 만든다면 AnnotationTarget.ANNOTATION_CLASS를 대상으로 선언하라.
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.METHOD)
annotation class JsonExclude
애노테이션 파라미터로 클래스 사용
정적인 인자가 아닌, 어떤 클래스를 애노테이션을 파라미터로 사용하는 방법을 보자! 직렬화된 Person 인스턴스를 역직렬화하는 과정에서 company 프로퍼티를 표현하는 json을 읽으면, 제이키드는 그 프로퍼티 값에 해당하는 JSON을 역직렬화하면서, CompanyImpl의 인스턴스를 만들어 Person 인스턴스의 company 프로퍼티에 설정한다. 이렇게 역직렬화에 사용할 클래스를 지정하기 위해 DeserializeInterface 애노테이션 인자에 CompanyImpl::class를 넘긴다.
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
)
KClass는 자바 java.lang.class타입과 같은 역할을 하는 코틀린 타입이다. 이 KClass는 리플렉션에서 사용한다.
애노테이션 파라미터로 Generic Class 받기
제이키드 라이브러리는 기본적으로 원시타입이 아닌 프로퍼티를 중첩된 객체로 직렬화 한다. 이런 기본동작을 변경하고 싶으면 *@CustomSerializer**를 쓰고 직렬화하는 로직을 제공해야한다.
data class Person(
val name: String,
@CustomSerializer(DateSerializer::class) val birthDate: Date
)
interface ValueSerializer<T> {
fun toJsonValue(value: T): Any?
fun fromJsonValue(jsonValue: Any?): T
}
object DateSerializer : ValueSerializer<Date> {
private val dateFormat = SimpleDateFormat("dd-mm-yyyy")
override fun toJsonValue(value: Date): Any? =
dateFormat.format(value)
override fun fromJsonValue(jsonValue: Any?): Date =
dateFormat.parse(jsonValue as String)
}
리플렉션
리플렉션은 실행시점에(동적으로) 객체의 프로퍼티와 메소드에 접근할수 있게 해주는 방법이다. 예를 들어서 직력화 라이브러리는 실행시점이 되기 전까지는 직렬화할 프로퍼티나 클래스의 정보를 알수가 없으므로, 리플렉션을 써야 한다.
- java.lang.reflect 자바가 제공하는 표준 리플렉션. 코틀린은 자바의 reflect도 완벽히 지원한다.
- kotlin.reflect 이 API는 자바에는 없는 프로퍼티나 Nullable타입과 코틀린 고유 개념에 대한 리플렉션을 제공한다. 현재 코틀린 리플렉션 라이브러리 자바 리플렉션 API를 대체할 수 있는 복잡한 기능을 제공하지 않는다..
코틀린 리플렉션 API : KClass, KCallable, KFunction, KProperty
java.lang.class에 해당하는 KClass를 사용하면 클래스 안에 모든 선언을 열거나 각 선언에 접근이 가능하다. 그리고, 실행시점에 객체의 클래스를 얻으려면 먼저 객체의 javaClass 프로퍼티를 사용해 객체의 자바 클래스를 얻어야 한다. javaClass는 자바의 *java.lang.Object.getClass()*와 같다. 일단 자바 클래스를 얻었으면, kotlin 확장 프로퍼티를 통해 자바에서 코틀린 리플렉션 API로 옮겨올수 있다.
class Person(val name: String, val age: Int)
>>> import kotlin.reflect.full.* # memberProperties 확장함수 import
>>> 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에 대해 사용할 수 있는 다양한 기능은 실제로 kotlin-reflect 라이브러리를 통해 제공하는 확장함수다. 이런 확장함수를 사용하기 위해서는 import kotlin.reflect.full. *로 확장함수를 import해야 한다. 또한 KCallable은 함수와 프로퍼티를 아우르는 공통 상위 인터페이스이다. 그안에 call메소드가 있다.
interface KClass<T : Any>
{
val simpleName: String?
val qualifiedName: String?
val members: Collection<KCallable<*>>
val constructors: Collection<KFunction<T>>
val nestedClasses: Collection<KClass<*>>
...
}
interface KCallable<out R> {
fun call(vararg args: Any?): R
...
}
kFunction
리플렉션 call을 사용해 함수를 호출하는 방법은 아래와 같다. ::foo식의 값 타입이 리플렉션 API에 있는 *KFunction 클래스**의 인스턴스임을 알수있다. 이 함수 참조가 가르키는 함수를 호출하려면 KCallable.call메소드를 호출한다.
fun foo(x: Int) = println(x)
>>> val kFunction = ::foo
>>> kFunction.call(42)
42
KFunction보다 더 구체적인 메소드를 사용할 수도 있다. ::foo이 타입 **KFunction1<Int, Unit>**에는 파라미터와 반환값 정보가 들어있다. 1은 파라미터가 1개라는 의미다. KFunction1을 통해 함수를 호출하려면 invoke를 사용해야 한다.
fun sum(x: Int, y: Int) = x + y
>>> val kFunction: KFunction2<Int, Int, Int> = ::sum
>>> println(kFunction.invoke(1, 2) + kFunction(3, 4))
10
>>> kFunction(1)
ERROR: No value passed for parameter p2
kProperty
kProperty에도 call함수를 호출할 수 있다. 이는 내부적으로 getter를 호출한다. 그러므로, getter를 가져와서 호출하는 것이 더 좋다.
>>> var counter = 0
>>> val kProperty = ::counter
>>> kProperty.setter.call(21)
>>> println(kProperty.get())
21
member property는 kProperty1이면, 한개의 argument를 갖는 get methtod를 가지고 있다. 첫번째 param에는 접근하려는 object instance를 전달해야 한다.
class Person(val name:String, val age:Int)
>>> val person = Person(“Alice”, 29)
>>> val memberProperty = Person::age
>>> println(memberProperty.get(person))
>>> 29
리플렉션을 사용한 객체 직렬화 구현
buildString은 stringBuilder를 만들고, lambda를 이용해 fill하도록 한다. 이 serializeObject를 수업도중 아래에서 계속 수정할것이다.
private fun StringBuilder.serializeObject(obj: Any){
val kClass = obj.javaClass.kotlin
val properties = kClass.memberProperties
properties.jointToStringBuilder(this, prefix = “{“, postfix = “}”) {
prop -> // KProperty1<Any, *> type
serializeString(prop.name) // JSON format 으로 escape 한다
append(“: “)
serializePropertyValue(prop.get(obj)) // primitive, string, collection, nested object 로 serialize
}
>>> fun serialize(obj: Any): String = buildString { serializeObject(obj) }
애노테이션을 활용한 직렬화 제어
지금까지 애노테이션 정의를 살펴봤다. 특히 @JsonExclude, @JsonName, @CustomSerializer 애노테이션에 대해 알아봤다. 이제 이런 애노테이션을 serializeObject함수가 어떻게 처리하는지 살펴볼 때다. kAnnotatedElement interface는 annotations라는 property를 가지고 있다. 이는 annotation instance collection이다 (runtime retiontion)
@JsonExclude
inline fun <reified T> KAnnotatedElement.findAnnotation(): T? =
annotations.filterIsInstance<T>().firstOrNull() // inline함수 정의
// JSONExclude annotaion이 적용안된 property를 찾는다. (Lambda)
val properties = kClass.memberProperties.filter {
it.findAnnotation<JSONExclude>() == null }
// 최종적으로 아래와 같은 serializeObject가 나온다.
private fun StringBuilder.serializeObject(obj: Any){
obj.javaClass.kotlin.memberProperty
.filter { it.findAnnotation<JSONExclude>() == null }
.jointToStringBuilder(this, prefix="{", postfix="}") {
serializeProperty(it, obj);
}
}
@JsonName
data class Person {
@JsonName('alias') val fistNAme: String,
val age: Int
}
// JsonName annotaion이 적용된 annoation을 찾는다.
val jsonNameAnn = prop.findAnnotation<JsonName>()
val propName = jsonNameAnn?.name ?: prop.name
// 최종적으로 아래와 같은 serializeProperty가 나온다.
private fun StringBuilder.serializeProperty(prop: kProperty<Any, *>, obj: Any){
val jsonNameAnn = prop.findAnnotation<JsonName> ()
val propName = jsonNameAnn?.name ?: prop.name
serializeString(propName)
append(":")
serializePropertyValue(prop.get(obj))
}
@CustomSerializer
여기서 가장 흥미로운 부분은 @CustomSerializer의 값으로 클래스와 객체(코틀린의 싱글턴 객체)를 처리하는 방식이다. 클래스와 객체는 모두 KClass클래스로 표현된다. 다만 객체에는 object 선언에 의해 생성된 싱글턴을 가르키는 objectInstance라는 프로퍼티가 있다는 것이 클래스와 다른 점이다. 예를 들어 DateSerializer를 객체로 선언한 경우에는 objectInstance프로퍼티에 DateSerializr의 싱글턴 인스턴스가 들어있다. 따라서 그 싱글턴 인스턴스를 사용해 모든 객체를 직렬화하면 되므로 createInstance를 호출할 필요가 없다.
// 애노테이션 선언
annotation class CustomSerializer (
val serializerClass: KClass<out ValueSerializer<*>>
)
data class Person(
val name: String,
@CustomSerializer(DateSerializer::class)
val birthDate: Date
)
// getSerializer가 주로 다루는 객체가 KProperty이므로, KProperty의 확장함수로 만든다.
fun KProperty<*>.getSerializer(): ValueSerializer<Any?>?{
val customSerializerAnn = findAnnotation<CustomSerializer>() ?: return null
val serializerClass = customSerializerAnn.serializerClass
//객체가 아니면, createInstance()로 인스턴스를 만들어 쓴다.
val valueSerializer = serialzierClass.objectInstance ?: serializerClass.createInstance()
@Suppress(“UNCHECKED_CAST”)
return valueSerializer as ValueSerializer<Any?>
}
ValueSerializer가 적용된 serializeProperty 최종버전
private fun StringBuilder.serializeProperty(prop: KProperty1<Any, *>, obj: Any){
val name = prop.findAnnotation<JsonName>()?.name ?: prop.name
serializeString(name)
append(“: “)
val value = prop.get(obj)
// KProperty<*>.getSerializer() 확장함수가 있으면 쓰고, 없으면 prop.get(obj)를 쓴다.
val jsonValue = prop.getSerializer()?.toJsonValue(value) ?: value
serializePropertyValue(jsonValue)
}
JSON parsing and object deserialization
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”}}”””
val book = deserialize<Book>(json)
println(book)
JKid 의 deserializer 는 lexical analyzer(lexer), syntax analyzer, 그리고 parser 로 구성되어 있다.
lexical analysis 는 string 을 token 으로 나눈다. 두가지 형태의 token 이 있는데 character token ( comma, colon, braces, brackets ) 과 value token ( string, number, boolean, null ) 이 있다. parser 는 plain token list 를 structured form 으로 변경하는 것을 이야기한다. JKid 는 JSON 이 structured form 이다.
Final deserialization step: callBy() and creating objects using reflection
KCallable.call 은 function 이나 constructor 를 호출할 수 있고, argument 들을 list 형태로 받을 수 있다. 많은 경우에 잘 작동하지만 제약사항이 있다. default parameter value 를 제공하지 않는다.
interface KCallable<out R> {
fun callBy(args: Map<KParameter, Any?>): R
....
}
callBy 는 전달하는 Map 에 빠진 param 이 있다면 default value 가 자동으로 사용된다. 즉 default value 를 잘 지원한다. Map 으로 전달하니, named-argument 를 사용하는 것처럼 순서에도 영향을 받지 않는다. Primary constructor 를 호출하는 데에도 사용될 수 있다.