BottleH Blog

Kotlin in Action - 12장 애노테이션과 리플렉션

    Tags

  • Kotlin
Kotlin in Action - 12장 애노테이션과 리플렉션 thumbnail

📖 12.1 어노테이션 선언과 적용

🔖 12.1.1 어노테이션을 적용해 선언해 표지 남기기

  • 코틀린에서 어노테이션을 적용하려면 @와 어노테이션 이름을 선언 앞에 넣으면 된다.
@Deprecated("Use removeAt(index) instead.", ReplaceWith("removeAt(index)")) fun remove(index: Int) { }
  • @Deprecated 어노테이션은 최대 3가지 파라미터를 받는다.
    • message: 사용중단 예고 이유
    • replaceWith: 옛 버전을 대신할 수 있는 패턴 제시
    • level: 점진적인 사용 중단을 지원 제공
      • WARNING: 경고만 함
      • ERROR: 컴파일되지 못하게 막음.
      • HIDDEN: 컴파일되지 못하게 막으며, 예전에 컴파일된 코드와의 이진호환성만 유지
  • 코틀린에서 어노테이션 인자를 지정하는 문법은 자바와 약간 다르다.
    • 클래스를 어노테이션 인자로 지정: ::class를 클래스 이름 뒤에 넣어야 한다.
    • 다른 어노테이션을 인자로 지정: 인자로 들어가는 어노테이션의 이름 앞에 @를 넣지 말라.
    • 배열을 인자로 지정: 각괄호([])를 사용, arrayOf 함수 사용
  • 어노테이션 인자를 컴파일 시점에 알 수 있어야 한다.
    • 즉, 임의의 프로퍼티를 인자로 지정할 수는 없다.
    • 프로퍼티를 인자로 사용하려면 const 변경자를 붙여야 한다.

🔖 12.1.2 어노테이션이 참조할 수 있는 정확한 선언 지정: 어노테이션 타깃

  • 사용 지점 타깃 선언은 통해 어노테이션을 붙일 요소를 정할 수 있다.
    • @ 기호와 어노테이션 이름 사이에 붙으며 어노테이션 이름과는 콜론으로 분리된다.
    • @get:JvmName("obtainCertificate")
@JvmName("performCalculation") fun calculate(): Int { return (2 + 2) - 1 }
  • calculate 함수를 자바 쪽에서 performCalculation()로 호출하게 한다.
class CertificateManager { @get:JvmName("obtainCertificate") @set:JvmName("putCertificate") var certificate: String = "-----BEGIN CERTIFICATE-----" }
  • certificate 프로퍼티를 obtainCertificate, putCertificate 라는 이름으로 사용할 수 있다.

사용 지점 타깃을 지정할 때 지원하는 타깃 목록

  • property: 프로퍼티 전체
  • field: 프로퍼티에 의해 생성되는 필드
  • get: 프로퍼티 게터
  • set: 프로퍼티 세터
  • receiver: 확장 함수나 프로퍼티의 수신 객체 파라미터
  • param: 생성자 파라미터
  • setparam: 세터 파라미터
  • delegate: 위임 프로퍼티의 위임 인스턴스를 담아둔 필드
  • file: 파일 안에 선언된 최상위 함수와 프로퍼티를 담아두는 클래스

코틀린에는 컴파일러 경고를 무시하기 위한 @Suppress 어노테이션이 있다.

🔖 12.1.3 어노테이션을 활용해 JSON 직렬화 제어

  • 직렬화는 객체를 저장 장치에 저장하거나 네트워크를 통해 전송하기 위해 텍스트나 이진 형식으로 변환하는 것
  • 역직렬화는 텍스트나 이진 형식으로 저장된 데이터에서 원래의 객체를 만들어낸다.
data class Person(val name: String, val age: Int) fun main() { val person = Person("Alice", 29) println(serialize(person)) }
  • Person 인스턴스를 serialize 함수에 전달하면 JSON 표현이 담긴 문자열을 돌려받는다.
fun main() { val json = """{"name": "Alice", "age": 29"}""" println(deserialize<Person>(json)) }
  • JSON 표현을 다시 코틀린 객체로 만들려면 deserialize 함수를 호출한다.
data class Person( @JsonName("alias") val firstName: String, @JsonExclude val age: Int? = null )
  • @JsonExclude 어노테이션을 사용하면 직렬화나 역직렬화할 때 무시해야 하는 프로퍼티를 표시할 수 있다.
    • 기본값 지정 필요
  • @JsonName 어노테이션을 사용하면 프로퍼티를 표현하는 키/값 쌍의 키로 프로퍼티 이름 대신 어노테이션이 지정한 문자열을 쓰게 할 수 있다.

🔖 12.1.4 어노테이션 선언

annotation class JsonExclude
  • 어노테이션 클래스는 선언이나 식과 관련 있는 메타데이터의 구조만 정의하기 때문에 내부에 아무 코드도 들어있을 수 없다.
    • 컴파일러는 어노테이션 클래스에서본문을 정의하지 못하게 막는다.
annotation class JsonName(val name: String)
  • 파라미터가 있는 어노테이션을 정의하려면 어노테이션 클래스의 주 생성자에 파라미터를 선언해야 한다.
  • 주 생성자 구문을 사용하며, val로 선언

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

  • 메타어노테이션: 어노테이션 클래스에 적용할 수 있는 어노테이션
@Target(AnnotationTarget.PROPERTY) annotation class JsonExclude
  • @Target 메타어노테이션은 어노테이션을 적용할 수 있는 요소의 유형을 지정
  • 어노테이션 클래스에 대해 구체적인 @Target을 지정하지 않으면 모든 선언에 적용할 수 있는 어노테이션이 된다.
@Target(AnnotationTarget.ANNOTATION_CLASS) annotation class BindingAnnotation @BindingAnnotation annotation class MyBinding
  • 메타어노테이션을 직접 만들어야 한다면 ANNOTATION_CLASS를 타깃으로 지정하자.

🔖 12.1.6 어노테이션 파라미터로 클래스 사용

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 )
  • @DeserializeInterface는 인터페이스 타입인 프로퍼티에 대한 역직렬화를 제어할 때 쓰는 어노테이션이다.
annotation class DeserializeInterface(val targetClass: KClass<out Any>)
  • 클래스 참조를 인자로 받는 어노테이션은 위와 같이 정의한다.

🔖 12.1.7 어노테이션 파라미터로 제네릭 클래스 받기

  • 기본적으로 제이키드는 기본 타입이 아닌 프로퍼티를 내포된 객체로 직렬화한다.
    • 이런 기본 동작을 변경하고 싶으면 값을 직렬화하는 로직을 직접 제공하면 된다.
interface ValueSerializer<T> { fun toJsonValue(value: T): Any? fun fromJsonValue(jsonValue: Any?): T }
  • 이 인터페이스는 코틀린 객체에서 JSON 표현으로의 변환을 제공하며, 반대의 경우도 제공한다.
data class Person( val name: String, @CustomSerializer(DateSerializer::class) val birthDate: Date ) annotation class CustomSerializer(val serializerClass: KClass<out ValueSerializer<*>>)
  • 클래스를 어노테이션 인자로 받아야 할 때마다 같은 패턴 사용이 가능하다.
    • KClass<out 자신의 클래스 이름>
  • 자신의 클래스 이름 자체가 타입 인자를 받아야 한다면 KClass<out 자신의 클래스 이름<*>> 처럼 타입인자를 *로 바꾼다.

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

  • 리플렉션은 실행 시점에 객체의 프로퍼티와 메서드에 접근할 수 있게 해주는 방법

🔖 12.2.1 코틀린 리플렉션 API: KClass, KCallable, KFunction, KProperty

interface KClass<T : Any> { val simpleName: String? val qualifiedName: String? val members: Collection<KCallable<*>> val constructors: Collection<KFunction<T>> val nestedClasses: Collection<KClass<*>> // ... }
  • KClass 선언을 찾아보면 클래스 내부를 살펴볼 때 사용할 수 있는 다양하고 유용한 메서드를 볼 수 있다.
interface KCallable<out R> { fun call(vararg args: Any?): R // ... }
  • KCallable은 함수와 프로퍼티를 아우르는 공통 상위 인터페이스다.
  • call을 사용할 때는 함수 인자를 vararg 리스트로 전달
fun foo(x: Int) = println(x) fun main() { val kFunction = ::foo kFunction.call(42) }
  • ::foo 식의 값 타입이 KCallable 클래스의 인스턴스이다.
fun sum(x: Int, y: Int) = x + y fun main() { val kFunction = KFunction2<Int, Int, Int> = ::sum println(kFunction.invoke(1, 2) + kFunction.invoke(3, 4)) kFunction(1) // error }
  • invoke 메서드를 호출할 때는 인자 개수나 타입을 실수로 틀릴 수 없다. 컴파일이 안되기 때문
var counter = 0 fun main() { val kProperty = ::counter kProperty.setter.call(21) println(kProperty.get()) }
  • 최상위 읽기 전용과 가변 프로퍼티는 각각 KProperty0이나, KMutableProperty0 인터페이스의 인스턴스로 표현
    • 인자 없는 get method 제공
  • KProperty1은 제네릭 클래스이다.

🔖 12.2.2 리플렉션을 사용해 객체 직렬화 구현

fun serialize(obj: Any): String
  • 제이키드의 직렬화 함수 선언이다.
  • 이 함수는 결과 JSON을 StringBuilder 인스턴스 안에 구축한다.
private fun StringBuilder.serializeObject(x: Any) { append(/*...*/) }
  • 별도로 수신 객체를 지정하지 않고 append 메서드를 편하게 사용할 수 있다.
fun serialize(obj: Any): String = buildString { serializeObject(obj) }
  • 대부분의 작업을 serializeObject에 위임한다.
private fun StringBuilder.serializeObject(obj: Any) { val kClass = obj::class as KClass<Any> val properties = kClass.memberProperties properties.joinToStringBuilder( this, prefix = "{", postfix = "}" ) { prop -> serializeString(prop.name) append(": ") serializePropertyValue(prop.get(obj)) } }
  • 클래스의 각 프로퍼티를 차례로 직렬화 한다.

🔖 12.2.3 어노테이션을 활용해 직렬화 제어

어노테이션을 지원하기 위해 serializeObject를 어떻게 수정할지 알아본다.

val properties = kClass.memberProperties.filter { it.findAnnotation<JsonExclude>() == null }
  • 위와 같이 @JsonExclude 어노테이션이 붙지 않은 프로퍼티만 남길 수 있다.
val jsonNameAnn = prop.findAnnotation<JsonName>() val propName = jsonNameAnn?.name ?: prop.name
  • 어노테이션에 전달한 인자도 알아야 한다.
private fun StringBuilder.serializeObject(obj: Any) { (obj::class as KClass<Any>) .memberProperties .filter { it.findAnnotation<JsonExclude>() == null } .joinToStringBuilder(this, prefix = "{", postfix = "}") { serializeProperty(it, obj) } }
  • @JsonExclude 어노테이션한 프로퍼티를 제외시킨다.
private fun StringBuilder.serializeObject(prop: KProperty1<Any, *>, obj: Any) { val jsonNameAnn = prop.findAnnotation<JsonName>() val propName = jsonNameAnn?.name ?: prop.name serializeString(propName) append(": ") serializePropertyValue(prop.get(obj)) }
  • @JsonName에 따라 프로퍼티 이름을 처리한다.
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?> }
  • 프로퍼티의 값을 직렬화하는 직렬화기 가져오기
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) }
  • serializeProperty 구현 안에서 getSerializer를 사용할 수 있다.

🔖 12.2.4 JSON 파싱과 객체 역직렬화

  • 제이키드의 JSON 역직렬화기는 3단계로 구현돼 있다.
    1. 어휘 분석기(렉서)
      • 토큰의 리스트로 변환(문자토큰, 값 토큰)
    2. 문법 분석기(파서)
      • 토큰의 리스트를 구조화된 표현으로 변환
    3. 역직렬화 컴포넌트
      • 필요한 클래스의 인스턴스를 생성해 반환

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

interface KCallable<out R> { fun callBy(args: Map<KParameter, Any?>): R // ... }
  • KCallable.call은 디폴트 파라미터 값을 지원하지 않는다.
  • KCallable.callBy는 디폴트 파라미터 값을 지원한다.
object BooleanSerializer : ValueSerializer<Boolean> { override fun fromJsonValue(jsonValue: Any?): Boolean { if (jsonValue !is Boolean) throw JkidException("Boolean Expected") return jsonValue } override fun toJsonValue(value: Boolean) = value }
  • Boolean 값을 위한 직렬화기
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> }
  • 리플렉션 데이터 캐시 저장소
  • 성능을 위해 검색 결과를 캐시에 넣어둔다.
Written by@BottleH
Back-End Developer

GitHub