리플렉션은 프로그램 구조 내부를 동적으로(런타임 때) 들여다볼 수 있는 언어 혹은 라이브러리 기능의 집합이다.
함수형, 반응형 프로그래밍에서는 사용할 함수, 프로퍼티를 동적으로(런타임 시에) 결정할 일이 많다. 따라서 리플렉션(예를 들어, 런타임에 1급 시민인 프로퍼티 또는 함수의 이름이나 유형을 학습하는 것)은 필수적이다.
fun greet() {
println("Hello!")
}
fun main() {
greet() // 컴파일 타임에 "greet()" 함수가 실행될 것임이 확정됨
}
fun greet() {
println("Hello!")
}
fun bye() {
println("Goodbye!")
}
fun main() {
val input = readln() // 사용자 입력을 받음 (실행될 때까지 어떤 값이 들어올지 모름)
if (input == "hi") {
greet() // 런타임에 결정됨
} else {
bye() // 런타임에 결정됨
}
}
JVM 플랫폼은 Kotlin 컴파일러 배포에 리플렉션 기능을 위해 kotlin-reflect.jar
를 런타임 컴포넌트로써 포함할 수 있다.
kotlin-reflect.jar
는 리플렉션을 사용하지 않는 애플리케이션의 경우 라이브러리 크기를 줄이기 위해 별도의 아티팩트로 선언되어있다.
.jar
(JAR, Java ARchive): Java 라이브러리를 담고 있는 압축 파일.class
파일(바이트코드)을 하나로 파일로 묶은 것리플렉션을 Gradle 프로젝트에서 사용하기 위해서는 kotlin-reflect
에 대한 의존성을 추가해주어야 한다.
dependencies {
implementation(kotlin("reflect"))
}
가장 기본적인 리플렉션 기능은 Kotlin 클래스의 런타임 참조를 얻는 것이다. 정적으로 알려진 Kotlin 클래스에 대한 참조를 얻으려면 class literal 구문을 사용할 수 있다.
::class
class MyClass
fun main() {
val c: KClass<MyClass> = MyClass::class
}
KClass<T>
자료형을 갖는다.JVM에서 Kotlin 클래스 참조는 Java 클래스 참조와 다르다. Java 클래스 참조를 얻으려면
KClass
인스턴스의.java
프로퍼티를 사용할 수 있다.
객체를 리시버로 사용하는 ::class
구문으로 해당 객체의 클래스에 대한 참조를 얻을 수 있다.
fun main() {
val string: Any = ""
println(string::class.qualifiedName)
}
결과:
kotlin.String
Any
)에 무관하게 실제 객체의 타입(String
)을 얻게 된다.함수, 프로퍼티, 생성자에 대한 참조 또한 호출되거나, 함수 유형의 인스턴스로 사용될 수 있다.
모든 호출 가능한(callable) 참조에 대한 공통 슈퍼타입은 KCallable<out R>
이다. 타입 파라미터 R
은 반환 값 타입을 의미한다. 따라서 프로퍼티 타입의 경우엔 프로퍼티의 타입이고, 생성자의 경우 생성된 타입이다.
class MyClass {
val property = ""
fun function1() {}
fun function2() {}
}
fun main() {
val property: KCallable<String> = MyClass::property // 실제 타입: KProperty<String>
val constructor: KCallable<MyClass> = ::MyClass // 실제 타입: KFunction0<MyClass>
val function1: KCallable<Unit> = MyClass::function1 // 실제 타입: KFunction1<MyClass, Unit>
val function2: KCallable<Unit> = MyClass::function2 // 실제 타입: KFunction1<MyClass, Unit>
}
아래와 같은 기명 함수는 isOdd(5)
와 같이 호출된다.
fun isOdd(x: Int) = x % 2 != 0
위 함수를 함수 타입의 변수로 사용하고 싶다면, ::
연산자를 사용할 수 있다.
val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd))
::isOdd
는 함수 타입 (Int) → Boolean
이 저장된 변수이다.
함수 참조는 KFunction<out R>
의 하위 타입이다. 파라미터 개수에 따라 달라지는데, 예를 들어 매개변수가 3개라면 KFunction3<T1, T2, T3, R>
이 된다.
::
는 예상 타입을 문맥을 통해 알 수 있을 경우, 오버로드된 함수에도 사용될 수 있다.
fun isOdd(x: Int) = x % 2 != 0
fun isOdd(s: String) = s == "brillig" || s == "slithy" || s == "tove"
val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd)) // refers to isOdd(x: Int)
반대로 메서드 참조를 변수에 저장할 때 타입을 명시적으로 선언해 필요한 문맥을 제공할 수도 있다.
val predicate: (String) -> Boolean = ::isOdd // refers to isOdd(x: String)
클래스의 멤버 또는 확장을 사용하려면, String::toCharArray
형태로 사용할 수 있다.
확장 함수에 대한 참조를 변수에 저장하더라도, 추론된 타입에는 리시버가 없다. 대신 리시버를 첫 번째 파라미터로 추가된 형태로 존재한다.
만약 리시버가 존재하는 타입으로 저장하려면, 타입을 명시하면 된다.
fun main() {
val isEmptyStringList1: (List<String>) -> Boolean = List<String>::isEmpty
val isEmptyStringList2: List<String>.() -> Boolean = List<String>::isEmpty
}
예시: 함수 조합
fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
return { x -> f(g(x)) }
}
위 함수는 전달된 두 함수를 조합한다.(compose(f, g) = f(g(*))
) 이를 callable 참조에 적용할 수 있다.
fun length(s: String) = s.length
val oddLength = compose(::isOdd, ::length)
val strings = listOf("a", "ab", "abc")
println(strings.filter(oddLength)) // filter를 한 번만 호출
코틀린에서 프로퍼티들을 일급 객체로서 접근하려면, ::
연산자를 사용할 수 있다.
val x = 1
fun main() {
println(::x.get())
println(::x.name)
}
결과
1
x
표현식 ::x
는 KProperty0<Int>
타입의 프로퍼티 객체로 간주된다. KProperty0<Int>
는 게터를 사용해 값을 읽거나, name
프로퍼티를 통해 프로퍼티 이름을 얻을 수 있다.
public actual interface KCallable<out R> : KAnnotatedElement {
...
public actual val name: String
...
}
var y = 1
과 같이 가변으로 선언된 프로퍼티의 경우 KMutableProperty0<Int>
타입의 객체로 간주된다.
var y = 1
fun main() {
::y.set(2)
println(y)
}
결과
2
프로퍼티 참조는 하나의 제네릭 파라미터가 존재하는 함수가 필요할 때 사용할 수 있다.
val strs = listOf("a", "bc", "def")
println(strs.map(String::length))
결과
[1, 2, 3]
확장 프로퍼티의 경우에는 다음과 같다.
fun main() {
class A(
val p: Int,
)
val prop = A::p
println(prop.get(receiver = A(1)))
}
결과
1
JVM 플랫폼 스탠다드 라이브러리는 자바 리플렉션 객체와 상호 운용을 위한 여러 확장 기능들을 제공한다. 예를 들어 필드나 게터 메서드를 찾기 위해, 아래와 같이 작성할 수 있다.
import kotlin.reflect.jvm.*
class A(val p: Int)
fun main() {
println(A::p.javaGetter) // prints "public final int A.getP()"
println(A::p.javaField) // prints "private final int A.p"
}
자바 클래스와 일치하는 코틀린 클래스를 얻으려면 .kotlin
확장 프로퍼티를 사용할 수 있다.
fun getKClass(o: Any): KClass<Any> = o.javaClass.kotlin
public val <T : Any> Class<T>.kotlin: KClass<T>
@JvmName("getKotlinClass")
get() = Reflection.getOrCreateKotlinClass(this) as KClass<T>
생성자 또한 메서드나 프로퍼티처럼 참조될 수 있다. 생성자와 파라미터 형식이 같고, 해당 타입의 객체를 반환하는 함수(형 객체)를 필요로 하는 곳이라면 생성자를 참조해 사용할 수 있다. 생성자는 ::
연산자 뒤에 클래스 이름을 붙여 참조할 수 있다. 다음 예시는 파라미터가 없고, 반환값이 Foo
인 함수를 파라미터로 받는 함수이다.
class Foo
fun function(factory: () -> Foo) {
val x: Foo = factory()
}
Foo
클래스의 파라미터가 없는 생성자를 ::Foo
를 통해 참조하면 다음과 같이 사용할 수 있다.
생성자에 대한 참조는 파라미터의 개수에 따라 타입이 결정되며, KFunction<out R>
의 하위 타입이다.
특정 객체의 메서드 또한 참조할 수 있다.
val numberRegex = "\\d+".toRegex()
println(numberRegex.matches("29"))
val isNumber = numberRegex::matches
println(isNumber("29"))
isNumber
는 numberRegex
의 matches
에 대한 참조를 사용하고 있다. 이러한 참조는 참조값의 리시버(이 경우 numberRegex
)에 구속된다. 위 예시처럼 호출(()
)하거나, 아래처럼 함수 타입으로 사용할 수 있다.
val numberRegex = "\\d+".toRegex()
val strings = listOf("abc", "124", "a70")
println(strings.filter(numberRegex::matches))
bound된 참조의 타입과 unbound된 참조의 타입은 다음과 같이 비교된다. bound된 callable 참조는 리시버가 존재하기 때문에, 더 이상 파라미터로 쓰이지 않는다.
val isNumber: (CharSequence) -> Boolean = numberRegex::matches
val matches: (Regex, CharSequence) -> Boolean = Regex::matches
프로퍼티 참조 또한 bound될 수 있다.
val prop = "abc"::length
println(prop.get())
리시버에 대해 this
를 명시해줄 필요는 없다. this::foo
와 ::foo
는 똑같다.
bound된 callable 참조를 사용해 외부 클래스의 인스턴스로 inner class의 클래스 생성자에 접근할 수 있다.
class Outer {
inner class Inner
}
val o = Outer()
val boundInnerCtor = o::Inner