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
KCallable
의 call
메소드는 모든 타입의 함수에 적용할 수 있지만, 타입 안전성을 보장해주지 않는다. KFunctionN
은 인자 타입과 반환 타입을 모두 안다면 이 방법을 사용하는 것이 더 낫다
합성 타입
KFunctionN
타입은 KFunction
을 확장하며 N
과 파라미터 개수가 같은 invoke
를 추가로 포함한다. 이런 타입은 컴파일러가 생성한 합성 타입이기 때문에 패키지에서 이런 타입의 정의를 찾을 수 없다. 이런 특성으로 인해 kotlin-runtime.jar의 크기를 줄이고 함수 파라미터 개수에 대한 인위적인 제약을 피할 수 있다.
KProperty
KProperty
의 call
은 프로퍼티의 게터를 호출한다. 그러나 프로퍼티 인터페이스는 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>
타입이다.
최상위 수준이나 클래스 안에 정의된 프로퍼티만 리플렉션으로 접근할 수 있고 함수의 로컬 변수에는 접근할 수 없다.