리플렉션

리플렉션 (Reflection)

리플렉션(reflection)은 런타임에 프로그램의 구조를 들여다볼 수 있게 해 주는 언어·라이브러리 기능의 집합이에요. Kotlin에서 함수와 프로퍼티는 일급 시민(first-class citizen)이라서, 이들을 런타임에 검사하는 능력(예를 들어 프로퍼티나 함수의 이름이나 타입을 알아내는 일)은 함수형 또는 반응형 스타일을 쓸 때 필수적입니다.

Kotlin/JS는 리플렉션 기능에 대해 제한적인 지원을 제공해요. Kotlin/JS에서의 리플렉션에 대해 자세히 알아보세요.

출처: Kotlin 공식 문서

본문

JVM 의존성

JVM 플랫폼에서 Kotlin 컴파일러 배포판은 리플렉션 기능을 사용하는 데 필요한 런타임 컴포넌트를 kotlin-reflect.jar라는 별도 아티팩트로 포함하고 있어요. 이는 리플렉션 기능을 사용하지 않는 애플리케이션의 런타임 라이브러리 크기를 줄이기 위해서입니다.

Gradle이나 Maven 프로젝트에서 리플렉션을 사용하려면 kotlin-reflect에 의존성을 추가해요.

  • Gradle에서:
dependencies {
    implementation(kotlin("reflect"))
}
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-reflect:2.4.20"
}
  • Maven에서:
<dependencies>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-reflect</artifactId>
    </dependency>
</dependencies>

Gradle이나 Maven을 사용하지 않는다면 프로젝트의 클래스패스에 kotlin-reflect.jar가 있는지 확인해요. 다른 지원되는 경우(명령줄 컴파일러를 사용하는 IntelliJ IDEA 프로젝트)에는 기본적으로 추가됩니다. 명령줄 컴파일러에서는 -no-reflect 컴파일러 옵션을 사용해 클래스패스에서 kotlin-reflect.jar를 제외할 수 있어요.

클래스 참조

가장 기본적인 리플렉션 기능은 Kotlin 클래스에 대한 런타임 참조를 얻는 것이에요. 정적으로 알려진 Kotlin 클래스에 대한 참조를 얻으려면 클래스 리터럴(class literal) 문법을 사용할 수 있습니다.

val c = MyClass::class

이 참조는 KClass 타입의 값입니다.

JVM에서: Kotlin 클래스 참조는 Java 클래스 참조와 같지 않아요. Java 클래스 참조를 얻으려면 KClass 인스턴스에서 .java 프로퍼티를 사용하세요.

바운드 클래스 참조

같은 ::class 문법을 객체를 리시버로 사용해 적용하면, 특정 객체의 클래스에 대한 참조를 얻을 수 있어요.

val widget: Widget = ...
assert(widget is GoodWidget) { "Bad widget: ${widget::class.qualifiedName}" }

리시버 표현식의 타입(Widget)과 무관하게 객체의 정확한 클래스(예: GoodWidget 또는 BadWidget)에 대한 참조를 얻을 수 있습니다.

호출 가능 참조 (Callable references)

함수, 프로퍼티, 생성자에 대한 참조도 호출할 수 있고, 함수 타입의 인스턴스로 사용할 수 있어요.

모든 호출 가능 참조의 공통 수퍼타입은 KCallable<out R>이며, 여기서 R은 반환 값 타입입니다. 프로퍼티의 경우 프로퍼티 타입이고, 생성자의 경우 생성된 타입이에요.

함수 참조

아래처럼 선언된 이름 있는 함수가 있다면 직접 호출할 수 있어요(isOdd(5)).

fun isOdd(x: Int) = x % 2 != 0

대안으로 함수를 함수 타입 값으로 사용해 다른 함수에 전달할 수도 있어요. 그러려면 :: 연산자를 사용합니다.

fun isOdd(x: Int) = x % 2 != 0

fun main() {
//sampleStart
    val numbers = listOf(1, 2, 3)
    println(numbers.filter(::isOdd))
//sampleEnd
}

여기서 ::isOdd는 함수 타입 (Int) -> Boolean의 값입니다.

함수 참조는 파라미터 수에 따라 KFunction<out R> 하위 타입 중 하나에 속해요. 예를 들어 KFunction3<T1, T2, T3, R>처럼요.

::는 컨텍스트에서 기대 타입을 알 수 있을 때 오버로드된 함수에도 사용할 수 있어요. 예를 들어:

fun main() {
//sampleStart
    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)
//sampleEnd
}

대안으로 메서드 참조를 명시적으로 타입이 지정된 변수에 저장해 필요한 컨텍스트를 제공할 수도 있어요.

val predicate: (String) -> Boolean = ::isOdd   // refers to isOdd(x: String)

클래스의 멤버나 확장 함수를 사용해야 한다면 한정(qualify)해야 해요: String::toCharArray처럼요.

확장 함수에 대한 참조로 변수를 초기화하더라도, 추론된 함수 타입에는 리시버가 없고 리시버 객체를 받는 추가 파라미터가 있을 뿐이에요. 리시버가 있는 함수 타입을 얻으려면 타입을 명시적으로 지정해야 합니다.

val isEmptyStringList: 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(*))). 이 함수를 호출 가능 참조에 적용할 수 있습니다.

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() {
//sampleStart
    fun length(s: String) = s.length
    
    val oddLength = compose(::isOdd, ::length)
    val strings = listOf("a", "ab", "abc")
    
    println(strings.filter(oddLength))
//sampleEnd
}

프로퍼티 참조

Kotlin에서 프로퍼티를 일급 객체로 접근하려면 :: 연산자를 사용해요.

val x = 1

fun main() {
    println(::x.get())
    println(::x.name) 
}

::x 표현식은 KProperty0<Int> 타입의 프로퍼티 객체로 평가됩니다. get()으로 값을 읽거나 name 프로퍼티로 프로퍼티 이름을 얻을 수 있어요. 자세한 내용은 KProperty 클래스 문서를 참고하세요.

var y = 1 같은 가변 프로퍼티의 경우 ::yset() 메서드를 가진 KMutableProperty0<Int> 타입의 값을 반환해요.

var y = 1

fun main() {
    ::y.set(2)
    println(y)
}

프로퍼티 참조는 단일 제네릭 파라미터를 가진 함수가 기대되는 곳에 사용할 수 있어요.

fun main() {
//sampleStart
    val strs = listOf("a", "bc", "def")
    println(strs.map(String::length))
//sampleEnd
}

클래스의 멤버인 프로퍼티에 접근하려면 다음과 같이 한정하세요.

fun main() {
//sampleStart
    class A(val p: Int)
    val prop = A::p
    println(prop.get(A(1)))
//sampleEnd
}

확장 프로퍼티의 경우:

val String.lastChar: Char
    get() = this[length - 1]

fun main() {
    println(String::lastChar.get("abc"))
}

Java 리플렉션과의 상호운용

JVM 플랫폼에서 표준 라이브러리에는 Java 리플렉션 객체와의 변환을 제공하는 리플렉션 클래스용 확장이 포함되어 있어요(kotlin.reflect.jvm 패키지 참고). 예를 들어 Kotlin 프로퍼티의 게터 역할을 하는 배킹 필드나 Java 메서드를 찾으려면 다음과 같이 작성할 수 있습니다.

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"
}

Java 클래스에 해당하는 Kotlin 클래스를 얻으려면 .kotlin 확장 프로퍼티를 사용해요.

fun getKClass(o: Any): KClass<Any> = o.javaClass.kotlin

생성자 참조

생성자는 메서드와 프로퍼티처럼 참조할 수 있어요. 프로그램이 생성자와 같은 파라미터를 받고 적절한 타입의 객체를 반환하는 함수 타입 객체를 기대하는 곳 어디에서든 사용할 수 있습니다. 생성자는 :: 연산자를 사용하고 클래스 이름을 붙여서 참조해요. 파라미터가 없고 반환 타입이 Foo인 함수 파라미터를 기대하는 다음 함수를 생각해 볼게요.

class Foo

fun function(factory: () -> Foo) {
    val x: Foo = factory()
}

클래스 Foo의 영(0) 인자 생성자인 ::Foo를 사용하면 이렇게 호출할 수 있어요.

function(::Foo)

생성자에 대한 호출 가능 참조는 파라미터 수에 따라 KFunction<out R> 하위 타입 중 하나로 타입이 지정됩니다.

바운드 함수·프로퍼티 참조

특정 객체의 인스턴스 메서드를 참조할 수 있어요.

fun main() {
//sampleStart
    val numberRegex = "\\d+".toRegex()
    println(numberRegex.matches("29"))
     
    val isNumber = numberRegex::matches
    println(isNumber("29"))
//sampleEnd
}

matches 메서드를 직접 호출하는 대신, 이 예시는 그 메서드에 대한 참조를 사용해요. 이런 참조는 리시버에 바인드되어 있습니다. (위 예시처럼) 직접 호출할 수도 있고, 함수 타입 표현식이 기대되는 곳 어디든 사용할 수 있어요.

fun main() {
//sampleStart
    val numberRegex = "\\d+".toRegex()
    val strings = listOf("abc", "124", "a70")
    println(strings.filter(numberRegex::matches))
//sampleEnd
}

바운드 참조와 언바운드 참조의 타입을 비교해 볼게요. 바운드 호출 가능 참조는 리시버가 "붙어" 있어서 리시버 타입이 더 이상 파라미터가 아니에요.

val isNumber: (CharSequence) -> Boolean = numberRegex::matches

val matches: (Regex, CharSequence) -> Boolean = Regex::matches

프로퍼티 참조도 바인드할 수 있어요.

fun main() {
//sampleStart
    val prop = "abc"::length
    println(prop.get())
//sampleEnd
}

리시버로 this를 지정할 필요는 없어요. this::foo::foo는 동등합니다.

바운드 생성자 참조

내부 클래스 생성자에 대한 바운드 호출 가능 참조는 바깥 클래스의 인스턴스를 제공해 얻을 수 있어요.

class Outer {
    inner class Inner
}

val o = Outer()
val boundInnerCtor = o::Inner

더 알아보기 (Learn more)