Kotlin 리플렉션 API

리플렉션은 실행 시점에 동적으로 객체의 프로퍼티와 메소드에 접근할 수 있게 해주는 방법이다. 코틀린에서 리플렉션을 다루려면 자바의 java.lang.reflect와 코틀린의 kotlin.reflect를 모두 다룰 수 있어야 한다.

  • java.lang.reflect: 리플렉션을 사용하는 자바 라이브러리와 코틀린 코드가 호환된다.
  • kotlin.reflect: 자바에 없는 코틀린 고유 개념에 대한 리플렉션을 제공하지만 자바의 리플렉션 API를 완전히 대체하지 못한다.

KClass, KCallable, KFunction, KProperty

KClass

java.lang.Class에 대응하는 KClass를 사용하면 클래스 내의 모든 선언을 열거하고 각 선언에 접근하거나 클래스의 상위 클래스를 얻는 등의 작업이 가능해진다.

실행 시점에 객체의 클래스를 얻으려면 javaClass( = java.lang.Object.getClass())프로퍼티를 사용해 객체의 자바 클래스를 얻어야 한다. 얻은 자바 클래스에서 .kotlin확장 프로퍼티를 사용하면 KClass를 얻을 수 있다.

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

val person = Person("Alice", 29)
val kClass = person.javaClass.kotlin
println(kClass.simpleName) // Person
kClass.memberProperties.forEach { println(it.name) } // age\nname

Kclass 선언은 https://github.com/JetBrains/kotlin/blob/1.1.3/core/builtins/src/kotlin/reflect/KClass.kt에서 찾아볼 수 있다.

memberProperties를 비롯한 KClass에 사용 가능한 다양한 기능은 실제로는 kotlin-reflect라이브러리를 통해 제공하는 확장 한수이다. 이런 함수를 사용하려면 import kotlin.reflect.full.*로 확장 함수 선언을 임포트해야 한다.

KCallable

KCallable은 함수와 프로퍼티를 아우르는 공통 상위 인터페이스이다. KCallable 내부에는 call메소드가 들어있다. call을 사용하여 함수나 프로퍼티의 게터를 호출할 수 있다.

fun foo(x: Int) = println(x)
val kFunction = ::foo
kFunction.call(42) // 42

call을 사용할 때에는 함수 인자를 vararg 리스트로 전달한다. call에 넘긴 인자 개수와 원래 함수에 정의된 파라미터 개수가 맞아 떨어져야 한다. 그렇지 않으면 런타임 에ㄹ가 발생한다.

::foo의 값 타입은 리플렉션 API에 있는 KFunction 클래스의 인스턴스이다.

KFunction

함수를 호출하기 위해 더 구체적인 메소드를 사용할 수 있다. ::foo의 타입 KFunction1<Int, Unit>에는 파라미터와 반환 값 정보가 들어 있다. 1은 이 함수의 파라미터가 1개라는 의미이다. KFunction1인터페이스를 통해 함수를 호출하려면 invoke메소드를 호출하면 된다(또는 invoke관례에 의해 kFunction을 직접 호출할 수 있다).

fun sum(x: Int, y: Int) = x + y
val kFunction: KFunction2<Int, Int, Int> = ::sum
println(kFunction.invoke(1, 2) + kFunction(3, 4)) // 10

KCallablecall메소드는 모든 타입의 함수에 적용할 수 있지만, 타입 안전성을 보장해주지 않는다. KFunctionN은 인자 타입과 반환 타입을 모두 안다면 이 방법을 사용하는 것이 더 낫다

합성 타입

KFunctionN 타입은 KFunction을 확장하며 N과 파라미터 개수가 같은 invoke를 추가로 포함한다. 이런 타입은 컴파일러가 생성한 합성 타입이기 때문에 패키지에서 이런 타입의 정의를 찾을 수 없다. 이런 특성으로 인해 kotlin-runtime.jar의 크기를 줄이고 함수 파라미터 개수에 대한 인위적인 제약을 피할 수 있다.

KProperty

KPropertycall은 프로퍼티의 게터를 호출한다. 그러나 프로퍼티 인터페이스는 get메소드로 프로퍼티 값을 더 좋은 방법으로 얻을 수 있다.

get메소드에 접근하려면 프로퍼티가 선언된 방법에 따라 올바른 인터페이스를 사용해야 한다. 최상위 프로퍼티는 KProperty0 인터페이스의 인스턴스로 표시되며, KProperty0 안에는 인자가 없는 get메소드가 있다.

var counter = 0
val kProperty = ::counter
kProperty.setter.call(21)
println(kProperty.get()) // 21

멤버 프로퍼티는 KProperty1인스턴스로 표현된다. 그 안에는 인자가 1개인 get메소드가 있다. 멤버 프로퍼티는 어떤 객체에 속해있는 프로퍼티이므로 멤버 프로퍼티의 값을 가져오려면 get 메소드에 프로퍼티를 얻고자 하는 객체 인스턴스를 넘겨야 한다.

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

val person = Person("Alice", 29)
val memberProperty = Person::age
println(memberProperty.get(person)) // 29

여기서 memberProperty 변수는 KProperty<Person, Int> 타입이다.

최상위 수준이나 클래스 안에 정의된 프로퍼티만 리플렉션으로 접근할 수 있고 함수의 로컬 변수에는 접근할 수 없다.


© 2021. All rights reserved.

Powered by Hydejack v9.1.6