10. 애노테이션과 리플렉션
CS/코틀린 인 액션

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

(잘 이해가 안되서 이해한 부분 위주로만 작성한다.

나중에 코틀린에 익숙해지면 다시 읽고 오자)

10장에서 다루는 내용

- 애노테이션 적용과 정의

- 리플렉션을 사용해 실행 시점에 객체 내부 관찰

- 코틀린 실전 프로젝트 예제

 

실전 프로젝트란

https://github.com/yole/jkid

 

GitHub - yole/jkid: JSON serialization/deserialization library for Kotlin data classes

JSON serialization/deserialization library for Kotlin data classes - GitHub - yole/jkid: JSON serialization/deserialization library for Kotlin data classes

github.com

이 프로젝트를 말한다.

 

 

애노테이션을 사용하면 API나 리플렉션처럼 해당 함수의 내부 구조는 몰라도, 가져다 쓸 수 있다.

위의 jkid 공개 프로젝트가 그것으로, JSON을 직렬화와 역직렬화하는 간단한 코드로 되어 있다. 간단하니 모든 코드를 보면서 이 장을 보라고 한다.

 

10.1 애노테이션 선언과 적용

애노테이션 문법은 다음과 같다.

@get:Rule

get은 사용 지점 대상, Rule은 애노테이션 이름.

class HasTempFolder {
    @get:Rule
    val folder = TemporaryFolder()
    
    @Test
    fun testUsingTempFolder() {
        val createdFile = folder.newFile("myfile.txt")
        val createdFolder = folder.newFolder("subfolder")
        // ...
    }
}

 

이제 JSON의 직렬화와 역직렬화의 예를 볼건데, 위의 jkid를 빌려와 다음과 같이 사용한다.

data class Person{
    @JsonName("alias") val name: String
    @JsonExclude val age: Int? = null
}

선언문 앞에 @를 붙여서 사용한다.

annotation class JsonExclude

JsonName과 JsonExclude는 애노테이션 클래스인데, 오직 선언이나 식과 관련 있는 메타데이터(metadata)의 구조를 정의하기 때문에 아무 코드도 들어있을 수 없다. 그래서 컴파일러는 애노테이션 클래스에서 본문을 정의하기 못하게 막는다.

 

애노테이션에도 애노테이션을 붙일 수 있는데, 애노테이션 클래스에 적용할 수 있는 애노테이션을 메타에노테이션(meta-annotation)이라고 부른다.

@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude

원래 애노테이션이 변수 유형을 지정했다면, 메타 애노테이션은 애노테이션을 적용할 수 있는 요소의 유형을 지정한다.

 

애노테이션 파라미터로 클래스를 인자로 받아 사용할 땐 다음과 같이 한다.

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
)

고정변수타입(Int, String)같은건 역직렬화할 때(json 형태 문자열에서 객체로 바꿀 때) 알아서 타입을 알아내서 변환할 수 있지만(사실 jkid 안에서 when으로 일일히 지정해주긴 함), 그냥 만든 클래스 타입의 경우 모르기 때문에 어떤 오브젝트(클래스)로 역직렬화 해줘야 할 지 알려주는 것이다.

하지만 DeserializeInterface의 인자로 뭐가 들어갈 지 모른다. 어떻게 정의하는지 보자.

annotation class DeserializeInterface(val targetClass: KClass<out Any>)

KClass 같은건 자바의 java.lang.Class같은 역할을 한다. 즉, 모든 클래스의 조상. 뭐가 올지 몰라 Any로 설정하고 클래스격의 Any인 KClass로 지정한다.

 

제네릭 클래스도 받을 수 있다.

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

annotation class CustomSerializer(
    val serializerClass: KClass<out ValueSerializer<*>>
)

 

 

10.2 리플렉션: 실행 시점에 코틀린 객체 내부 관찰

간단히 말해 리플렉션은 실행 시점에(동적으로) 객체의 프로퍼티와 메서드에 접근할 수 있게 해주는 방법이다. 보통 객체의 메서드나 프로퍼티에 접근할 때는 프로그램 소스코드 안에 구체적인 선언이 있는 메서드나 프로퍼티 이름을 사용하며, 컴파일러는 그런 이름이 실제로 가리키는 선언을 컴파일 시점에(정적으로)찾아내서 해당하는 선언이 실제 존재함을 보장한다. 하지만 타입과 관계없이 객체를 다뤄야 하거나 객체가 제공하는 메서드나 프로퍼티 이름을 오직 실행 시점에만 알 수 있는 경우가 있다. 실행 시점 전까지 알 수 없지만 다뤄야 하므로 이럴 때 리플렉션을 사용한다.

코틀린에서 리플렉션을 사용하려면 두 가지 서로 다른 리플렉션 API를 다뤄야 한다. 첫 번째는 자바가 java.lang.reflect 패키지를 통해 제공하는 표준 리플렉션이고, 두 번째 API는 코틀린이 kotlin.reflect 패키지를 통해 제공하는 코틀린 리플렉션 API다.

 

코틀린이 자바의 클래스를 그대로 사용하므로 동적 변수를 다룰 때 javaClass를 직접 가져와 사용한다.

data class Person(val name: String, val age: Int)

>>> import kotlin.reflect.full.* // memberProperties 확장 함수 임포트
>>> val person = Person("Alice", 29)
>>> val kClass = person.javaClass.kotlin
>>> println(kClass.simpleName) // Person
>>> KClass.memberProperties.forEach { println(it.name) } // name, age

call을 사용함

interface KCallable<out R> {
    fun call(): R
}

fun foo(x: Int) = println(x)
>>> val kFunction: KCallable<Unit> = ::foo
>>> kFunction.call(42)
// 42

KFunction0, KFunction1, KFunction2, ...인 KFunctionN은 함수 파라미터 갯수에 따라 달라진다

 

private fun StringBuilder.serializeObject(obj: Any) {
    val kClass = obj.javaClass.kotlin
    val properties = kClass.memberProperties
    properties.joinToStringbuilder(
        this, prefix = "{", postfix = "}") { property ->
            serializeString(property.name)
            append(":")
            serializeValue(property.get(obj))
        }
}

어떤 함수이고 역할인지 명확히 하기 위해 클래스를 확장하는 형식으로 정의한다.

 

 

앞에서 했던 jsonNameAnn이나 Exclude가 붙은 것들은 findAnnotation으로 해당 애노테이션이 붙었는지 안붙었는지 구분해서 다르게 행동한다.

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

 

역직렬화의 경우, 오브젝트를 받고 그 안에서 또 오브젝트를 받고 ... 해야하기 때문에 재귀적으로 정의한다.

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