[kotlin] 리플렉션을 통한 인스턴스의 속성값 읽기

개요top

저번 포스팅에서는 간단히 JSON파서를 구현하면서 언어에 대한 감을 익혔습니다. 이번에 런타임에 리플렉션을 통해 인스턴스의 속성 값을 읽어오도록 해볼까 합니다.
코틀린은 순수한 코틀린 리플렉션 API를 제공합니다. 기본적인 기능 외에 보다 많은 확장 기능을 사용하려면

implementation “org.jetbrains.kotlin:kotlin-reflect:1.3.11”

이런 식의 추가적인 패키지 설치를 요구합니다.

memberPropertiestop

kotlin.reflect.full.memberProperties에 정의 되어있는 확장 기능을 이용하면 손쉽게 클래스의 속성을 읽을 수 있습니다.
특정 인스턴스의 속성을 동적으로 읽고 싶다면 다음과 같이 정리할 수 있습니다.

class Test{
  val v1 = 0.0
  val v2 = "abc"
}
val test = Test()

val kclass = test::class
kclass.memberProperties.forEach{
  print(it.name + "::" + it.getter.get(test))
}
//v1::0.0
//v2::abc

java패키지를 쓸 필요도 없고 인터페이스도 훨씬 간편하게 빠져있습니다(내부적으로는 복잡한 제네릭인터페이스지만요)
우선 ::class를 이용해 인스턴스의 KClass를 얻어온 뒤 멤버를 순회해보면 그 안에 KProperty2를 얻을 수 있습니다.
이 안에는 name과 getter가 있고 최종 getter를 이용해 대상 인스턴스를 넘겨주면 그 인스턴스의 해당 속성 값을 얻을 수 있는 구조입니다.

캐쉬top

헌데 안드로이드 같은 플랫폼에서 리플렉션은 심각한 속도 저하를 가져올 수 있습니다. 리플렉션이 필요하지만 리플렉션이 느린 것을 보완하기 위한 인류의 도전은 계속 되어왔습니다 두둥.

  1. byte코드 생성기 – 미친..
  2. 애노테이션으로 코드 대신 생성 – 미친..

저는 차마 저런 것을 할 수는 없고 되도록이면 런타임에 리플렉션 트리를 순회하지 않고 한 번 탐색한 이후에는 쭉 확정된 getter를 사용하게 하는 정도에서 타협을 봤고 그 정도로 크게 나쁘지 않았습니다.
KClass는 태생부터 복잡한 제네릭 형태로 그 안에 있는 서브 인터페이스들은 점점 더 제네릭 인자가 늘어나는 지옥입니다.
어쩔 수 없이 *와 Any를 이용하지 않으면 캐쉬 레이어를 범용으로 제작하는 것이 불가능합니다.
우선 캐쉬를 잡을 맵을 하나 구성합니다.

import kotlin.reflect.KProperty1.Getter

private val cache = mutableMapOf<KClass<*>, Map<String, Getter<*, *>>>()

KProperty1은 T, R 두 개의 제네릭을 받는데 이는 곧장 Getter에 전달되어 인자로 전달될 인스턴스의 형과 속성의 형에 대응하여 값을 주게 됩니다..만 무시하고 *로 잡아서 범용 캐쉬가 될 수 있게 합니다.
그럼 캐쉬에 삽입할 수 있는 방법도 필요할 것입니다.

fun setCache(cls: KClass<*>):Map<String, Getter<*, *>>{
    if(cache[cls] != null) return cache[cls]!!
    val fields = mutableMapOf<String, Getter<*, *>>()
    cls.memberProperties.forEach{fields[it.name] = it.getter}
    cache[cls] = fields
    return fields
}

이제 아무 클래스에서나 리플렉션 정보를 캐쉬화할 수 있습니다.

class Test{
  val a = 5.5
  private val fields = setCache(this::class)
}

하지만 캐쉬에서 꺼내쓸 수도 있어야겠죠. 이를 사용하는 간단한 test메소드를 하나 정의합니다.

class Test{
  val a = 5.5
  private val fields = setCache(this::class)
  fun test():Double = ref["a"]!!.call(this) as Double
}
Test().test() //5.5

결론top

코틀린의 리플렉션은 java패키지의 도움 없이도 충분히 편리하게 사용 가능하고 편리하며 명쾌하네요.
앱에서 사용되는 범용 뷰모델이나 엔티티는 뷰델바인딩을 직접 구현해보면 모델 측의 정적 속성을 뷰 선언에 맞춰 매핑하고 싶은 경우가 많이 생깁니다. 이러한 간단한 코드로 이 문제를 큰 성능저하 없이 처리할 수 있습니다.
전체 코드는 아래와 같습니다.

import kotlin.reflect.full.memberProperties
import kotlin.reflect.KProperty1.Getter

private val cache = mutableMapOf<KClass<*>, Map<String, Getter<*, *>>>()

fun setCache(cls: KClass<*>):Map<String, Getter<*, *>>{
    if(cache[cls] != null) return cache[cls]!!
    val fields = mutableMapOf<String, Getter<*, *>>()
    cls.memberProperties.forEach{fields[it.name] = it.getter}
    cache[cls] = fields
    return fields
}

class Test{
  val a = 5.5
  private val fields = setCache(this::class)
  fun test():Double = ref["a"]!!.call(this) as Double
}
Test().test() //5.5