마흔한 번째, 리플렉션입니다.
리플렉션(reflection)은 실행 시간에 프로그램 구조를 살펴볼 수 있게 해 주는 언어와 라이브러리의 기능 집합입니다. Kotlin에서 함수와 프로퍼티는 일급 시민(first-class citizens)이고, 함수형이나 반응형 스타일을 사용할 때, 이들의 내부를 살펴볼 수 있는 능력(예: 실행 시간에 프로퍼티나 함수의 이름이나 타입을 아는 것)은 필수입니다.
※ Kotlin/JS는 제한적인 리플렉션 기능을 제공합니다. 자세한 내용은 여기를 참고하세요.
※ 리플렉션은 명사지만, 현업에서는 '리플렉션 하다' 같이 내부를 살펴보는 행위를 나타내는 동사로도 사용합니다.
JVM 의존성
JVM 플랫폼에서 Kotlin 컴파일러 배포판은 리플렉션 기능을 사용하는 필요한 별도의 실행 시간 컴포넌트 kotlin-reflect.jar를 포함하고 있습니다. 리플렉션 기능을 사용하지 않는 애플리케이션에게 요구되는 실행 시간 라이브러리의 사이즈를 줄이기 위해서 별도로 분리돼 있습니다.
Gradle이나 Maven 프로젝트에서 리플렉션을 사용하기 위해서는 다음과 같이 kotlin-reflect 의존성을 추가해야 합니다.
→ Gradle
// Kotlin
dependencies {
implementation(kotlin("reflect"))
}
// Groovy
dependencies {
implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.23"
}
→ Maven
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
</dependencies>
Gradle이나 Maven을 사용하지 않는 경우에는 프로젝트의 클래스 패스에 kotlin-reflect.jar를 추가해야 합니다. 명령줄 컴파일러를 사용하는 IntelliJ IDEA나 Ant 같은 경우에는 기본적으로 추가됩니다. 이 경우에 클래스 패스에서 kotlin-reflect.jar를 제외하려면 -no-reflect 옵션을 사용할 수 있습니다.
클래스 참조
가장 기본적인 리플렉션 기능은 Kotlin 클래스의 실행 시간 참조를 얻는 것입니다. 정적으로 알려진 Kotlin 클래스의 참조를 얻기 위해서는 다음과 같은 클래스 리터럴 구문을 사용할 수 있습니다.
val c = MyClass::class
이 참조는 KClass 타입 값입니다.
※ JVM에서 Kotlin 클래스의 참조는 Java 클래스의 참조와 같지 않습니다. Java 클래스 참조를 얻기 위해서는 KClass 인스턴스의 .java 프로퍼티를 사용합니다. 예) MyClass::class.java
객체의 클래스 참조 (Bound class references)
객체에 연결된 또는 묶인 클래스를 참조한다는 뜻으로 원문의 소제목은 Bound class references 입니다.
이후로도 bound functions and properties references, bound constructor refereces가 나오는데 바운드 함수 참조나 연결된 -, 경계가 있는 - 등등으로 번역하지 않고 모두 객체의 것들을 참조하는 것이라 그 의미를 풀어서 객체의 ... 참조 식으로 표기했습니다. 참고 부탁 드립니다.
특정 객체의 클래스 참조를 얻을 때도는 똑같은 구문인 ::class를 해당 객체를 수신자로 하여 사용합니다.
val widget: Widget = ...
assert(widget is GoodWidget) { "Bad widget: ${widget::class.qualifiedName}" }
이렇게 하면 Widget를 사용했지만, 정확한 클래스의 참조(여기서는 GootWidget나 BadWidget)를 얻게 됩니다.
호출 가능한 참조
함수, 프로퍼티, 생성자의 참조는 호출하거나 함수 타입의 인스턴스로 사용할 수 있습니다.
모든 호출 가능한 타입의 공통 수퍼 타입은 KCallable<out R> 인데 R은 반환 타입입니다. 이는 프로퍼티를 위한 프로퍼티의 타입이자 생성자를 위한 생성자의 타입입니다.
함수 참조
다음과 같이 선언된 이름이 있는 함수가 있을 때, isOdd(5) 같이 직접적으로 호출할 수 있습니다.
fun isOdd(x: Int) = x % 2 != 0
다른 방법으로, 함수를 함수 타입 값으로 사용할 수 있습니다. 즉, 다른 함수에 전달할 수 있습니다. 이렇게 하기 위해서는 :: 연산자를 사용합니다.
fun isOdd(x: Int) = x % 2 != 0
fun main() {
val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd))
}
/* 결과
[1, 3]
*/
여기서 ::isOdd는 함수 타입 (Int) -> Boolean의 값입니다.
함수 참조는 매개변수의 수에 따라 KFunction<out R>의 하위 타입 중 하나에 속합니다. 예, KFunction3<T1, T2, T3, R>
예상되는 타입을 문맥(context)을 통해 알 수 있으면 오버로딩된 함수에 대해서도 :: 를 사용할 수 있습니다. 예를 들면, 다음과 같습니다.
fun main() {
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)) // isOdd(x: Int)를 참조
}
/* 결과
[1, 3]
*/
다른 방법으로, 메소드 참조를 명시적으로 지정된 타입과 함께 변수에 저장하여 필요한 문맥을 제공할 수도 있습니다.
val predicate: (String) -> Boolean = ::isOdd // isOdd(x: String) 참조
클래스나 확장 함수의 멤버를 사용할 필요가 있다면, 앞쪽에 한정자(qualifier)를 붙여야 합니다. 예, String::toCharArray
변수를 확장 함수의 참조로 초기화 할지라도 추론된 함수 타입은 수신자를 갖지 않습니다. 하지만, 수신자 객체를 받는 추가적인 매개변수를 가지게 됩니다. 수신자가 있는 함수 타입을 갖기 위해서는 타입을 명시적으로 지정해야 합니다.
// 이렇게 선언하면
// val isEmptyStringList = List<String>::isEmpty
// isEmptyStringList(listOf()) 는 가능하나
// listOf<String>().isisEmptyStringList() 로는 사용할 수 없습니다.
// 다음과 같이 명시적으로 타입을 지정해 주어야 listOf<String>().isisEmptyStringList()로도 사용할 수 있습니다.
val isEmptyStringList: List<String>.() -> Boolean = List<String>::isEmpty
예: 함수 합성 (composition)
다음 함수를 살펴 보겠습니다.
fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
return { x -> f(g(x)) }
}
이 함수는 전달 받은 두 함수의 합성 함수를 반환합니다, compose(f, g) = f(g(*)). 이 함수를 호출 가능한 참조로 사용할 수 있습니다.
fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
return { x -> f(g(x)) }
}
fun isOdd(x: Int) = x % 2 != 0
fun main() {
fun length(s: String) = s.length
val oddLength = compose(::isOdd, ::length)
val strings = listOf("a", "ab", "abc")
println(strings.filter(oddLength))
}
/* 결과
[a, abc]
*/
프로퍼티 참조
Kotlin에서 프로퍼티를 일급 객체(first-class object)로서 접근하기 위해서는 :: 연산자를 사용합니다.
val x = 1
fun main() {
println(::x.get())
println(::x.name)
}
::x는 KProperty0<Int> 타입의 프로퍼티 객체로 평가됩니다(evaluate). get()을 사용하여 x의 값을 읽을 수 있습니다. 그리고, name 프로퍼티를 사용하여 이름을 확인할 수도 있습니다. 보다 자세한 정보는 KProperty 클래스 문서를 참고하시기 바랍니다.
var y = 1 같은 가변 프로퍼티에 대해서 ::y는 set() 메소드를 가진 KMutableProperty0<Int> 타입의 객체를 반환합니다.
var y = 1
fun main() {
::y.set(2)
println(y) // 2
}
클래스 멤버인 프로퍼티를 접근하기 위해서는 다음과 같이 클래스 이름으로 한정 시킵니다.
fun main() {
class A(val p: Int)
val prop = A::p
println(prop.get(A(1))) // 1
}
다음은 확장 프로퍼티에 대한 예입니다.
val String.lastChar: Char
get() = this[length - 1]
fun main() {
println(String::lastChar.get("abc")) // c
}
Java 리플렉션과 상호 운용
JVM 플랫폼에서 표준 라이브러리는 Java의 리플렉션 객체와 상호 매핑되는 리플렉션 클래스를 위한 확장을 포함하고 있습니다(kotlin.reflect.jvm을 보세요). 예를 들어, Kotlin 프로퍼티를 위한 뒷받침하는 필드나 게터로 동작하는 Java 메소드를 찾기 위해서 다음과 같이 할 수 있습니다.
import kotlin.reflect.jvm.*
class A(val p: Int)
fun main() {
println(A::p.javaGetter) // 출력: "public final int A.getP()" 출력
println(A::p.javaField) // 출력: "private final int A.p"
}
Java 클래스에 해당하는 Kotlin 클래스를 얻기 위해서는 .kotlin 이라는 확장 프로퍼티를 사용합니다.
fun getKClass(o: Any): KClass<Any> = o.javaClass.kotlin
생성자 참조
생성자는 메소드나 프로퍼티처럼 참조될 수 있습니다. 생성자와 매개변수가 같고 반환 객체가 적절한 타입(반환 타입이 생성자와 같거나 수퍼 타입인 경우)을 갖는 함수 타입 객체가 기대되는 곳 어디나 생성자를 사용할 수 있습니다. 생성자는 :: 연산자에 클래스 이름을 붙여서 참조합니다. 다음과 같이 매개변수가 없고 반환 타입이 Foo인 함수 매개변수를 갖는 함수를 살펴 보겠습니다.
class Foo
fun function(factory: () -> Foo) {
val x: Foo = factory()
}
Foo 클래스의 매개변수가 없는 사용하여 다음과 같이 호출할 수 있습니다.
function(::Foo)
생성자에 대한 호출할 수 있는 참조는 매개변수 수에 따라 KFunction<out R>의 하위 타입 중 하나입니다.
객체의 함수와 프로퍼티 참조 (Bound function and property references)
특정 객체의 메소드를 참조할 수 있습니다.
fun main() {
val numberRegex = "\\d+".toRegex()
println(numberRegex.matches("29"))
val isNumber = numberRegex::matches
println(isNumber("29"))
}
/* 결과
true
true
*/
예제에서는 matches 메소드를 직접 호출하는 대신에 해당 메소드의 참조를 사용합니다. 이러한 참조는 수신 객체와 묶여 있습니다. 이 참조는 위의 예제처럼 직접적으로 호출할 수도 있고, 함수 타입 표현식을 사용해야 하는 곳에 사용할 수 있습니다.
fun main() {
val numberRegex = "\\d+".toRegex()
val strings = listOf("abc", "124", "a70")
println(strings.filter(numberRegex::matches)) // 124
}
객체의 호출 가능 참조와 객체가 없는 호출 가능 참조를 비교해 보면, 지정된 참조는 해당 객체가 수신자여서 매개변수에 수신자 타입이 있지 않습니다.
val isNumber: (CharSequence) -> Boolean = numberRegex::matches
val matches: (Regex, CharSequence) -> Boolean = Regex::matches
프로퍼티 참조도 객체를 지정할 수 있습니다.
fun main() {
val prop = "abc"::length
println(prop.get()) // 3
}
this는 수신자로 지정할 필요가 없습니다. this::Foo 와 ::Foo는 같습니다.
객체의 내부 클래스 생성자 참조 (Bound constructor references)
class Outer {
inner class Inner
}
val o = Outer()
val boundInnerCtor = o::Inner
'Kotlin' 카테고리의 다른 글
공식 문서로 배우는 코틀린 - 42. Scope functions (0) | 2024.03.18 |
---|---|
공식 문서로 배우는 코틀린 - 40. Destructuring declarations (0) | 2024.03.17 |
공식 문서로 배우는 코틀린 - 39. Annotations (0) | 2024.03.16 |
공식 문서로 배우는 코틀린 - 38. This expressions (0) | 2024.03.16 |
공식 문서로 배우는 코틀린 - 37. Equality (0) | 2024.03.16 |