Kotlin – Reflection (2) – With Kotlin API

WindSekirun (wind.seo)·2022년 4월 26일
0

이 글은 기존 운영했던 WordPress 블로그인 PyxisPub: Development Life (pyxispub.uzuki.live) 에서 가져온 글 입니다. 모든 글을 가져오지는 않으며, 작성 시점과 현재 시점에는 차이가 많이 존재합니다.

작성 시점: 2017-10-14

지난번엔 Java API로 Reflection 기능을 어떻게 수행했는지 알아보았는데 이번에는 Kotlin API로 어떻게 가져오는지 살펴보자.


Kotlin Reflection API는 Kotlin의 기본 라이브러리 상에 포함되어 있지 않다.

Kotlin Reflection API를 사용하기 위해서는 build.gradle 에 kotlin-reflect 의존성을 추가하면 된다.

compile group: 'org.jetbrains.kotlin', name: 'kotlin-reflect', version: '1.1.51'


예제

먼저 이전 글에서 쓰인 Transaction 클래스를 그대로 가져오자.

class Transaction(val id: Int, val amount: Double, var description: String = "Default Value") {
    fun validate() {
        if (amount > 10000) {
            println("$this is too large")
        }
    }
}

코틀린에서 Java API를 쓰려면 ::class.java로 써야 되는데, 이전 글에서도 언급했다싶이 ::class 자체는 KClass 라는 인터페이스를 반환하기 때문이다.

이전 글과 비슷하게 아래 정보를 가져오는 코드를 짜보자.

  • 클래스의 이름
  • 해당 클래스에 선언된 필드 와 그 필드의 타입
  • 해당 클래스에 선언된 함수
println(Transaction::class)
val classInfo = Transaction::class

classInfo.memberProperties.forEach {
    println("Property ${it.name} of type ${it.returnType}")
}

classInfo.functions.forEach {
 println("Functions ${it.name} of type ${it.returnType}")
}

호출해보면 Java API와는 다른 점을 느낄 수 있는데, Java API는 타입을 출력할 때 해당 타입이 코틀린 타입임에도 불구하고 자바로 치환되는 타입을 리턴한다.

하지만 Kotlin Reflection API는 자바로 치환되는 타입이 아닌 그대로의 타입을 출력한다.

class Transaction
Property amount of type kotlin.Double
Property description of type kotlin.String
Property id of type kotlin.Int
Functions validate of type kotlin.Unit
Functions equals of type kotlin.Boolean
Functions hashCode of type kotlin.Int
Functions toString of type kotlin.String

그 외 코틀린에서는 getter / setter 대신 해당 클래스의 멤버에 직접 접근하는 특성을 가지고 있어 함수로는 뜨지 않지만 validate 메소드와 추가적으로 equals, hashcode, toString 등이 구현되어 있는 것을 확인할 수 있다.

타입

그러면 지난 글에 언급한 '타입' 은 어떻게 불러올까.

먼저 메소드를 하나 만든다.

fun getKotlinType(obj: KClass<*>) {
    println(obj.qualifiedName)
}

파라미터로는 KClass를 받는데, 사실상의 모든 코틀린 상 클래스는 KClass를 구현하고 있으므로 어떠한 클래스가 올 지 예측할 수 없어 Generic 쪽에서 잠깐 언급했던 Star projection 을 사용했다.

그리고 getKotlinType(Transaction::class) 로 부르게 되면 Transaction가 튀어나온다.

그 외 추가 정보

특이하게도 생성자의 정보를 불러오는 기능이 있다.

classInfo.constructors.forEach {
    println("Constructor ${it.name} - ${it.parameters}")
}

Constructor <init> - [parameter #0 id of fun <init>(kotlin.Int, kotlin.Double, kotlin.String): Transaction,
                      parameter #1 amount of fun <init>(kotlin.Int, kotlin.Double, kotlin.String): Transaction,
                      parameter #2 description of fun <init>(kotlin.Int, kotlin.Double, kotlin.String): Transaction]

결과로는 생성자의 정보를 각각 반환한다.

생성자를 변수로 받기

Kotlin Reflection API에는 특별한 기능이 있는데, 생성자 자체를 KFunctions 라는 클래스 변수로 담아 호출을 할 수 있게 해주는 것이다.

클래스 변수로 담는 방법은 간단한데, 클래스 이름 앞에 :: 를 붙여주면 된다. 예) val constructor = ::Transaction

Type hint 기능으로 실제 타입을 보면 KFunction3<Int, Double, String, Transaction> 라는 타입이 나온다.

그리고 이 constructor 를 println 에 노출시키면 생성자의 정보가 나오게 된다.  fun <init>(kotlin.Int, kotlin.Double, kotlin.String): Transaction

변수로 담은 생성자를 활용하기

단순히 담는 것에 끝나는 것이 아니라 역으로 활용해서 해당 클래스의 인스턴스를 생성할 수 있다.

val transaction = constructor.call(1, 2000, "some description")

call 메서드는 varang를 받는데, 여기에 각각 해당하는 변수 값을 넣어주면 실제 Transaction 클래스의 인스턴스가 생성된다.

만일 Transaction 의 파라미터 중 맨 마지막 description 필드가 기본 값이 있어서 두 개만 넣는다면 IllegalArgumentException 예외를 발생시킨다.

정확히는 Exception in thread "main" java.lang.IllegalArgumentException: Callable expects 3 arguments, but 2 were provided. 라 뜨게 되는데 Callable 가 3개의 파라미터를 요구했으나 두 개의 파라미터 밖에 주어지지 않았다 라는 예외다.

이를 해결하기 위해서는 두 가지 방법이 있다.

변수의 위치를 기준으로 기본값이 없는 필드를 지정하거나, 해당 필드의 이름을 기준으로 기본값이 없는 필드를 지정하는 것이다.

위 두 방법은 공통적으로 callBy 라는 메소드를 사용하게 된다.

변수의 위치를 기준으로 지정하기

val transaction2 = constructor.callBy(mapOf(constructor.parameters[0] to 1, constructor.parameters[1] to 2000))

callBy 메소드는 Map<KParameter, Any?> 를 받는데, KParameter는 파라미터의 Reflection 타입이다.

해당 생성자 변수(constructor)의 파라미터 리스트(parameters) 에서 0번째와 1번째를 꺼내 각각 1과 2000을 지정해주면 나머지 하나는 기본 필드값이 들어가게 된다.

필드의 이름을 기준으로 지정하기

val idParam = constructor.parameters.first { it.name == "id" }
val amountParam = constructor.parameters.first { it.name == "amount" }

val transaction3 = constructor.callBy(mapOf(idParam to 1, amountParam to 2000))

각각 idParam, amountParam 라는 변수를 선언하고 생성자 변수의 파라미터 리스트에서 "id", "amount" 라는 이름으로 객체를 찾는다.

여기서 first 메서드가 T를 반환해서 idParam, amountParam 의 실제 타입은 KParameter가 된다.

이 것을 각각 callBy에 넘기면 역시 마찬가지로 나머지 하나에 기본 필드값이 들어가게 된다.

필드의 이름만 알 때 그 필드에 선언된 실제 값을 알아내기

val trans = Transaction(1, 20.0, "New Value")

val nameProperty = Transaction::class.memberProperties.find { it.name == "description" }

먼저 Transaction 의 인스턴스가 있다고 해보자.

그런데 갑자기 해당 인스턴스에 있는 필드의 실제 값을 알고 싶을 때, Transaction 의 KClass 에서 memberProperties 라는 이름의 Collection<KProperty1<T, *>> 기능을 사용한다.

Collection는 List와 비슷하게 Iterable를 구현하고 있는 인터페이스고, KProperty1은 Map.Entry<Key, Value> 와 비슷하다고 생각하면 될 것이다.

그리고 해당 memberProperties 에서 name 가 description 을 찾으면 KProperty<Transaction, *>가 리턴된다.

마지막으로 nameProperty?.get(trans)처럼 Transaction의 인스턴스를 넘기면 해당 인스턴스의 description 값인 New Value가 리턴되는 것이다.

마무리

Kotlin Reflection API는 Java Reflection API의 대부분 기능을 구현하면서도 Kotlin 만의 기능들을 제공하고 있다.

다음 글에서는 이 Kotlin Reflection API 을 사용하여 Custom Annotation 등을 만들어보려고 한다.

profile
Android Developer @kakaobank

0개의 댓글