리플렉션이란?
리플렉션(Reflection)은 런타임에 프로그램의 구조를 파악할 수 있도록 하는 언어 및 라이브러리 기능의 집합을 의미한다. 코틀린을 기준으로 쉽게 설명하면 런타임 시점에 동적으로 요소들(클래스, 프로퍼티, 함수 등)에 접근하여 구조를 파악하고 내부 요소들을 사용할 수 있도록 만들어놓은 일련의 기능들이라고 보면 될 거 같다. 보통 해당 요소의 런타임 참조를 리플렉션 객체로 얻어서 리플렉션 관련 기능들을 사용한다. 리플렉션 객체는 K로 시작하는 리플렉션 인터페이스의 구현체 객체로, 아래에서 구조를 자세히 살펴보도록 하자.
리플렉션 인터페이스 구조
코틀린 언어에서 제공하는 요소별로 인터페이스가 구분되어 설계되어 있는데, 해당 인터페이스 구현체를 얻어 코틀린의 리플렉션 기능들을 사용할 수 있게 된다. 아래는 리플렉션 인터페이스들의 구조와, 해당 인터페이스가 어떤 코틀린 요소를 기준으로 기능을 명세되어 있는지 설명이다.
이름 | 기준 요소 |
`KClassifier` | 클래스 혹은 타입 파라미터 |
`KAnnotatedElement` | 어노테이션을 사용할 수 있는 요소 |
`KTypeParameter` | 타입 파라미터 |
`KClass` | 클래스 |
`KCallable` | 호출 가능한 요소(함수, 프로퍼티, 생성자) |
`KParameter` | 함수, 프로퍼티 게터 및 세터, 확장함수 등에 전달되는 파라미터 |
`KType` | null이 가능하고 타입 파라미터로 선언될 수 있는 요소들(타입) |
`KFunction` | 함수 |
`KProperty` | 프로퍼티 |
위와 같이 리플렉션 인터페이스가 설계되어 있고, 각 인터페이스 별로 얻을 수 있는 내부 정보들이 달라지게 된다.
예를 들어, `KClass`의 경우 `simpleName`, `qualifiedName`, `members`, `constructors` 등등이 멤버로 선언되어 있고, 각각 클래스의 이름, 전체 이름, 멤버들, 생성자들 등을 얻어올 수 있다. 즉, 클래스라는 요소를 기준으로 하여 클래스 내부 정보들을 런타임 시점에 얻을 수 있도록 설계된 클래스라고 볼 수 있다.
마찬가지로 `KProperty`의 경우 `isConst`, `getter`, `isLateinit` 등등이 멤버로 선언되어 있고, 프로퍼티라는 코틀린 요소를 기준으로 하여 프로퍼티 내부 정보들을 런타임 시점에 얻을 수 있도록 설계되어 있다.
리플렉션으로 제공되는 기능이 너무 많아 다 설명하기는 힘들고, 나머지 더 자세한 구현은 Kotlin 공식 문서를 참고하거나 직접 리플렉션 클래스 코드를 살펴보면서 필요한 기능들을 연구해보면 좋을 것이다.
리플렉션의 사용
KClass
클래스에 대한 리플랙션 기능은 `KClass`의 런타임 참조를 얻어서 사용할 수 있다.
class Reflection(
val a: String,
val b: String,
) {
fun funA() {
TODO()
}
}
fun method1(): String {
val reflectionKClass = Reflection::class // class literal syntax
return reflectionKClass.members.joinToString { it.name }
}
fun method2(): String {
val reflection = Reflection("1", "2")
val reflectionKClass = reflection::class // 수신객체
return reflectionKClass.members.joinToString { it.name }
}
fun main() {
println(method1() == method2()) // true
}
`KClass`의 런타임 참조를 얻는 방법은 위 예시처럼 주로 2가지 방법을 사용한다. (이 외에 Java 리플랙션을 통해서 얻는 방법도 있다)
`method1` 함수에서 사용한 방법은 class literal syntax를 사용한 방법이다. `KClass`객체를 가져오려는 클래스명 뒤에 `::class`를 통해 얻을 수 있다.
`method2` 함수에서 사용한 방법은 실제 객체를 수신 객체로 사용하여 참조를 얻어오는 방법이다. 객체 뒤에 `::class`를 통해 런타임 참조 객체를 얻을 수 있다.
`KClass`는 클래스와 관련된 정보를 얻을 수 있으며, 제공하는 기능이 많기 때문에 코틀린 문서를 확인하여 필요한 기능을 사용하면 된다.
KFunction
함수, 생성자를 기준으로 리플렉션 기능들을 얻을 수 있는 인터페이스이다. 호출 가능한 요소를 기준으로 한 `KCallable`을 구현했다.
사용 방법은 종종 함수형 프로그래밍에서 사용했던 함수 참조 문법과 동일하다. 사실 원래 알고 있던 함수 참조 문법 자체가, `KFunction`의 개념이다.
fun isZero(num: Int): Boolean = num == 0
fun main() {
val blankFunc = ::isZero
val numbers = listOf(0, 1, 2, 3)
numbers.filter(blankFunc)
}
함수 참조 역시 `::`을 통해 얻을 수 있다. 위 코드에서 `blankFunc`은 `KFunction1`타입(파라미터의 수에 따라 1, 2, 3.. 로 구현체가 달라진다)의 객체이며, 해당 객체를 함수 참조를 사용하여 `filter`를 호출할 때 처럼 파라미터로 넣어 사용이 가능하다.
fun isZero(str: String): Boolean = str == "0"
fun isZero(num: Int): Boolean = num == 0
fun main() {
val numbers = listOf(0, 1, 2, 3)
numbers.map(::isZero) // (Int) -> Boolean
}
val stringZero: (String) -> Boolean = ::isZero // (String) -> Boolean
만약 오버로딩된 함수인 경우, 타입 추론이 가능하다면 위와 같이 적합한 함수를 참조하게 된다.
생성자의 경우도 `KFunction`을 통해 리플렉션 기능을 이용할 수 있는데, 함수에서 사용하는 `KFunction`처럼 사용하면 된다.
`KFunction`은 `KCallable`의 기능을 포함하여, `isInline`, `isSuspend` 등의 기능들을 제공한다. 상세 기능들은 KCallable 문서와 KFunction 문서를 참조하여 필요한 것을 사용하면 좋을 거 같다.
KProperty
프로퍼티를 기준으로 리플렉션 기능들을 명세해놓은 인터페이스이다. `KFunction`과 마찬가지로 `::`을 통해 `KProperty`를 얻을 수 있다.
val globalA = 0
val globalKProperty = ::globalA
class Reflection(
val a: String,
)
val kPropertyA1 = Reflection::a
val kPropertyA2 = Reflection("a")::a
전역 프로퍼티 및 멤버 프로퍼티에서 KProperty를 모두 생성할 수 있다.
class Person(
var height: Int,
)
fun main() {
val people = listOf(
Person(150),
Person(200),
Person(130),
)
people.sortedByDescending(Person::height)
}
위와 같은 상황에서 `KProperty`는 인자가 없는 함수의 `KFuntion`과 동일하게 사용 가능하다.
KCallable - bound / unbound reference
`KCallable`은 수신객체가 포함되었는지(bound), 포함되지 않았는지(unbound)에 따라 그 사용법이 달라진다. 아래 코드를 살펴보자.
data class Person(
var height: Int,
) {
fun isHigh(): Boolean {
return this.height > 100
}
}
fun main() {
val person = Person(150)
println(person::class == Person::class) // true
println(person::height == Person::height) // false
println(person::isHigh == Person::isHigh) // false
}
실제 객체로부터, 클래스로부터 `::` 연산자를 통해 `KClass`를 가져온 경우에 해당 `KClass` 객체는 같은 객체를 얻는다. 반면, `KProperty`, `KFunction`과 같은 `KCallable`은 객체가 서로 다르다.
객체로부터 `KCallable`을 얻어온 경우에는 수신객체가 포함된 객체(bound)로 얻을 수 있으며, 단순 클래스명으로 얻어온 경우에는 수신객체가 포함되지 않은 상태(unbound)의 객체를 얻는다.
두 객체는 타입부터 다르기 때문에, 실제 사용에서도 unbound된 객체인 경우 수신객체를 필요로 한다. 한번 아래의 코드를 보자.
fun main() {
val person = Person(150)
val boundKProperty = person::height // KMutableProperty<Int>
boundKProperty.set(200)
val unboundKProperty = Person::height // KMutableProperty1<Person, Int>
unboundKProperty.set(person, 200)
}
수신객체가 포함되지 않은 경우에는 위와 같이 `set`을 호출할 때 수신객체가 필요하며, 수신객체가 포함된 경우에는 필요하지 않다. 이는 `KFunction`도 마찬가지이다.
fun main() {
val person = Person(150)
val boundKProperty = person::class.members.filterIsInstance<KMutableProperty0<Int>>()
val unboundKProperty = person::class.members.filterIsInstance<KMutableProperty1<Person, Int>>()
println(boundKProperty) // []
println(unboundKProperty) // [var Person.height: kotlin.Int]
}
참고로 `KClass`로부터 `members`를 호출하여 얻는 `List<KCallable<*>>`은 모두 수신객체가 포함되지 않은 객체를 반환한다. 따라서 이렇게 얻어온 리플랙션 객체를 통해 무언가 작업할 때에는 항상 수신객체가 인자로 필요하게 된다.
지금까지 리플렉션에 대해 이론적인 부분만 다뤘는데, 솔직히 모든 기능들을 일일이 설명하는 건 너무 양도 많고 의미가 없을 거 같아 작성하진 않았다. 차라리 리플렉션을 사용하여 실제 업무에 사용할 수 있도록 예제같은 것들을 다루는 것이 좋을 거 같다고 판단했고, 추후에 따로 포스팅할 수 있도록 노력하겠다.