리플렉션은 실행 시점에 (동적으로) 객체의 프로퍼티와 메소드에 접근할 수 있게 해주는 방법이다. 보통 객체의 메소드나 프로퍼티에 접근할 때는 프로그램 소스코드 안에 구체적인 선언이 있는 메소드나 프로퍼티 이름을 사용하며, 컴파일러는 그런 이름이 실제로 가리키는 선언을 컴파일 시점에 (정적으로) 찾아내서 해당하는 선언이 실제 존재함을 보장한다.
하지만 타입과 관계없이 객체를 다뤄야 하거나 객체가 제공하는 메소드나 프로퍼티 이름을 오직 실행 시점에만 알 수 있는 경우가 있다. JSON 직렬화 라이브러리가 그런 경우다. 직렬화 라이브러리는 어떤 객체든 JSON으로 변환할 수 있어야 하고, 실행 시점이 되기 전까지는 라이브러리가 직렬화할 프로퍼티나 클래스에 대한 정보를 알 수 없다. 이런 경우 리플렉션을 사용해야 한다.
코틀린에서 리플렉션을 사용하려면 두 가지 서로 다른 리플렉션 API를 다뤄야 한다. 첫 번째는 자바가 java.lang.reflect 패키지를 통해 제공하는 표준 리플렉션이다. 코틀린 클래스는 일반 자바 바이트코드로 컴파일되므로 자바 리플렉션 API도 코틀린 클래스를 컴파일한 바이트코드를 완벽히 지원한다. 이는 리플렉션을 사용하는 자바 라이브러리와 코틀린 코드가 완전히 호환된다는 뜻이므로 특히 중요하다.
두 번째 API는 코틀린이 kotlin.reflect 패키지를 통해 제공하는 코틀린 리플렉션 API다.이 API는 자바에는 없는 프로퍼티나 널이 될 수 있는 타입과 같은 코틀린 고유 개념에 대한 리플렉션을 제공한다.
하지만 현재 코틀린 리플렉션 API는 자바 리플렉션 API를 완전히 대체할 수 있는 복잡한 기능을 제공하지는 않는다. 따라서 자바 리플렉션을 대안으로 사용해야 하는 경우가 생긴다.
또한 코틀린 리플렉션 API가 코틀린 클래스만 다룰 수 있는 것은 아니다. 다른 JVM 언어에서 생성한 바이트코드를 충분히 다룰 수 있다.
안드로이드와 같이 런타임 라이브러리 크기가 문제가 되는 플랫폼을 위해 코틀린 리플렉션 API는 자동으로 추가되지 않는다. 따라서 kotlin-relfection.jar 파일을 직접 추가하거나 gradle에 아래 의존성을 추가해야 한다.
dependencies {
implementation(kotlin("reflect"))
}
java.lang.Class에 해당하는 KClass를 사용하면 클래스 안에 있는 모든 선언을 열거하고 각 선언에 접근하거나 클래스의 상위 클래스를 얻는 등의 작업이 가능하다. MyClass::class라는 식을 쓰면 KClass의 인스턴스를 얻을 수 있다. 실행 시점에 객체의 클래스를 얻으려면 먼저 객체의 javaClass 프로퍼티를 사용해 객체의 자바 클래스를 얻어야 한다. javaClass는 자바의 java.lang.Object.getClass()와 같다. 일단 자바 클래스를 얻었으면 .kotlin 확장 프로퍼티를 통해 자바에서 코틀린 리플렉션 API로 옮겨올 수 있다.
class Person(val name: String, val age: Int)
fun main() {
val person = Person("Alice", 29)
val kClass = person.javaClass.kotlin
println(kClass.simpleName)
kClass.memberProperties.forEach { println(it.name) }
}
Person
age
name
KClass 선언을 찾아보면 클래스의 내부를 살펴볼 때 사용할 수 있는 다양한 메소드를 볼 수 있다.
@Suppress("ACTUAL_CLASSIFIER_MUST_HAVE_THE_SAME_MEMBERS_AS_NON_FINAL_EXPECT_CLASSIFIER_WARNING", "ACTUAL_CLASSIFIER_MUST_HAVE_THE_SAME_SUPERTYPES_AS_NON_FINAL_EXPECT_CLASSIFIER_WARNING") // Can be dropped after bootstrap update
public actual interface KClass<T : Any> : KDeclarationContainer, KAnnotatedElement, KClassifier {
/**
* The simple name of the class as it was declared in the source code,
* or `null` if the class has no name (if, for example, it is a class of an anonymous object).
*/
public actual val simpleName: String?
/**
* The fully qualified dot-separated name of the class,
* or `null` if the class is local or a class of an anonymous object.
*/
public actual val qualifiedName: String?
/**
* All functions and properties accessible in this class, including those declared in this class
* and all of its superclasses. Does not include constructors.
*/
@Suppress("NON_ACTUAL_MEMBER_DECLARED_IN_EXPECT_NON_FINAL_CLASSIFIER_ACTUALIZATION_WARNING") // Can be dropped after bootstrap update
override val members: Collection<KCallable<*>>
/**
* All constructors declared in this class.
*/
@Suppress("NON_ACTUAL_MEMBER_DECLARED_IN_EXPECT_NON_FINAL_CLASSIFIER_ACTUALIZATION_WARNING") // Can be dropped after bootstrap update
public val constructors: Collection<KFunction<T>>
/**
* All classes declared inside this class. This includes both inner and static nested classes.
*/
@Suppress("NON_ACTUAL_MEMBER_DECLARED_IN_EXPECT_NON_FINAL_CLASSIFIER_ACTUALIZATION_WARNING") // Can be dropped after bootstrap update
public val nestedClasses: Collection<KClass<*>>
...
}
memberProperties를 비롯해 KClass에 대해 사용할 있는 다양한 기능은 실제로는 kotlin-reflect 라이브러리를 통해 제공하는 확장 함수다. 이런 확장 함수를 사용하려면 import kotlin.reflect.full.*
로 확장 함수 선언을 임포트해야 한다.
클래스의 모든 멤버의 목록은 KCallable 인스턴스의 컬렉션이다. KCallable은 함수와 프로퍼티를 아우르는 공통 상위 인터페이스다. 그 안에는 call 메소드가 들어있다. call을 사용하면 함수나 프로퍼티의 게터를 호출할 수 있다.
call을 사용할 때는 함수 인자를 vararg 리스트로 전달한다. 다음 코드는 리플렉션이 제공하는 call을 사용해 함수를 호출할 수 있음을 보여준다.