본문 바로가기
프로그래밍/kotlin

[kotlin] 코틀린 차곡차곡 - 12. 리플렉션(reflection)

by 사바라다 2021. 8. 29.

안녕하세요. 오늘은 코틀린에서의 리플렉션(reflection)에 대해서 알아보도록 하겠습니다.

리플렉션(reflection) 이란

리플렉션이란 런타임에 프로그램의 클래스를 조사하기 위해서 사용되는 기술입니다. 즉, 프로그램이 실행중일 때 인스턴스 등을 통해 객체의 내부 구조 등을 파악할 수 있습니다. Spring에서는 이 기술을 적극적으로 활용하고 있습니다. 대표적으로 어노테이션의 활용을 들 수 있습니다. 멤버 변수에 어노테이션을 붙인다면 어떻게 활용할 수 있을까요? 바로 이 리플렉션을 이용하는 것입니다. 리플렉션을 이용해서 멤버변수에 해당 어노테이션이 붙어있다면이라는 조건문을 통해 처리할 수 있는 것입니다.

우리가 자주사용하는 jackson library의 field를 추가하는 메서드 코드를 보도록 하겠습니다. 아래 코드를 통해 어노테이션과 리플렉션을 이런식으로 이용한다는 사실을 알아두시면 좋을것 같습니다.

protected void _addFields(Map<String, POJOPropertyBuilder> props)
{
    ...[중략]...

    for (AnnotatedField f : _classDef.fields()) {

    ...[중략]...

        // @JsonAnySetter?
        if (Boolean.TRUE.equals(ai.hasAnySetter(f))) {
            if (_anySetterField == null) {
                _anySetterField = new LinkedList<AnnotatedMember>();
            }
            _anySetterField.add(f);
            continue;
        }
        if (implName == null) {
            implName = f.getName();
        }
        PropertyName pn;

    ...[하략]...
    }
}

코틀린에서의 리플렉션의 사용하기 위한 의존성

위에서 일반적인 리플렉션에 대해서 알아보았으니 오늘의 주제인 코틀린에서의 리플렉션에 대해서 조금 더 자세히 알아보도록 하겠습니다. 코틀린에서는 자바 리플렉션 뿐만 아니라 이를 코틀린 레벨로 추상화한 코틀린 리플렉션을 별도로 제공해주고 있습니다. 코틀린 리플렉션은 바로 적용할 수 없습니다. 이를 이욯아기 위해서는 아래와 같은 의존성이 필요합니다.

implementation "org.jetbrains.kotlin:kotlin-reflect:{kotlin_version}"

하지만 여러분이 IntelliJ IDEA IDE를 사용하고 계시다면 IDE 자체에서 이에 대한 기본적인 의존성을 이미 가지고 있습니다. 만약 리플렉션을 사용하고자 하지 않으신다면 컴파일러 옵션으로 -no-reflect를 걸어주시면 됩니다.

실습

그렇다면 이제 실제로 코틀린에서 리플렉션을 이용해보도록 하겠습니다. 오늘 리플렉션을 통해 구조를 살펴 볼 클래스 코드는 아래와 같습니디. 구성요소로는 primary constructor에 변수가 하나, 클래스 멤버 변수가 2개, public 메서드 1개, private 메서드 1개 그리고 companion object로 해서 invoke 메서드가 하나 있습니다.

@Karol
class Sabarada(
    private val first: Int
) {
    private val immutableSecret: Int = 2

    private var mutableSecret: Int = 3

    fun make(second: Int): Int {
        return first + second
    }

    private fun secretMake(): Int {
        return immutableSecret + mutableSecret
    }
}

리플렉션 접근

코틀린에서 리플렉션을 이용하기 위해서 사용하는 객체를 레퍼런스 객체라고 합니다. 이러한 레퍼런스 객체를 만드는 방법은 인스턴스를 활용하는 방법과 클래스를 이용하는 방법이 있습니다.

아래의 코드는 인스턴스를 생성 후 해당 인스턴스를 이용하여 레퍼런스 객체에 접근하는 방법입니다. 레퍼런스 객체에 접근하기 위해서 인스턴스에 ::class를 이용한 것을 알 수 있습니다.

val sabarada = Sabarada(5)
val kClass: KClass<out Sabarada> = sabarada::class

아래 코드는 인스턴스 없이 클래스 정보만을 이용해서 레퍼런스 객체를 만드는 방법입니다. 클래스 이름에 ::class를 붙임으로써 제작할 수도 있습니다.

val kClass: KClass<Sabarada> = Sabarada::class

두개의 차이는 제네릭 타입에 인스턴스로 생성하면 out Sabarada 처럼 상속을 고려한 부분이 함께 붙는다는 것입니디. 이렇게 만든 KClass 타입의 코틀린 레퍼런스 객체를 이용하면 아래와 같은 정보를 확인할 수 있습니다.

println("qualifiedName = ${kClass.qualifiedName}")              // qualifiedName = personal.project.lighthouse.annotations.Sabarada
println("isAbstract = ${kClass.isAbstract}")                  // isAbstract = false              
println("isCompanion = ${kClass.isCompanion}")                // isCompanion = false
println("isData = ${kClass.isData}")                          // isData = false
println("isFinal = ${kClass.isFinal}")                        // isFinal = true
println("typeParameters = ${kClass.typeParameters}")          // typeParameters = []
println("functions = ${kClass.functions}")                    // functions = [fun personal.project.lighthouse.annotations.Sabarada.make(kotlin.Int): kotlin.Int, fun personal.project.lighthouse.annotations.Sabarada.secretMake(): kotlin.Int, fun personal.project.lighthouse.annotations.Sabarada.equals(kotlin.Any?): kotlin.Boolean, fun personal.project.lighthouse.annotations.Sabarada.hashCode(): kotlin.Int, fun personal.project.lighthouse.annotations.Sabarada.toString(): kotlin.String]
println("primaryConstructor = ${kClass.primaryConstructor}")  // primaryConstructor = fun <init>(kotlin.Int): personal.project.lighthouse.annotations.Sabarada
println("memberProperties = ${kClass.memberProperties}")      // memberProperties = [val personal.project.lighthouse.annotations.Sabarada.first: kotlin.Int, val personal.project.lighthouse.annotations.Sabarada.immutableSecret: kotlin.Int, var personal.project.lighthouse.annotations.Sabarada.mutableSecret: kotlin.Int]
println("annotations = ${kClass.annotations}")                // annotations = [@personal.project.lighthouse.annotations.Karol()]

이러한 정보 이외에도 mebers를 이용하면 함수와 메서드 모두를 확인할 수 있는 등 다양한 기능을 제공해주고 있습니다.

리플렉션을 이용한 객체 생성

레퍼런스 객체를 이용하여 실제 인스턴스를 만들어보도록 하겠습니다. 이 방법에는 default constructor(기본 생성자)가 있을 경우와 없을경우의 두가지 종류가 있습니다. 기본 생성자는 코틀린 클래스에 primary consturctor가 없거나 primary consturctor의 모든값이 기본값을 가지고 있으면 생성 됩니다.

아래 코드는 기본 생성자가 있을 경우 입니다. createInstance 메서드를 통해서 instance를 생성하는 것을 확인할 수 있습니다. 하지만 이방법은 앞서 말씀드린 것 처럼 기본 생성자가 없는 클래스라면 Class should have a single no-arg constructor 에러가 발생합니다.

val kClass: KClass<Sabarada> = Sabarada::class
val instace = kClass.createInstance()
println("instace = ${instace}") // instace = personal.project.lighthouse.annotations.Sabarada@7e9206f2

그렇다면 그런 클래스는 어떻게 인스턴스를 만들까? 하면 아래와 같이만들 수 있습니다. primaryConstructor을 가져와서 call 메서드를 통해 강제로 실행시켜 만들어내는 것입니다.

val kClass: KClass<Sabarada> = Sabarada::class
val primaryConstructor = kClass.primaryConstructor
val instance = primaryConstructor?.call(5)

println("call?.first = ${call?.first}") // call?.first = 5
println("call = ${call!!}") // call = personal.project.lighthouse.annotations.Sabarada@1dd7e9a1

추가적으로 private로 primary constructor를 만들었다면 primary constructor에 대해서 isAccessible = true를 통해서 접근을 가능하게 만들도록 선 처리후 접근하여 생성할 수 있습니다.

리플렉션을 이용한 함수 실행

그렇다면 리플렉션을 통해서 함수를 실행시켜 보도록 하겠습니다. 우리의 예제에는 make, secretMake의 두개의 보이는 함수와 보이지 않는 기본적으로 제공하는 toString, equals 등의 함수가 있습니다.

먼저 private fun secretMake()를 호출해보도록 하겠습니다. 호출을 하기 위해서 먼저 instance를 생성하고 KFunction 레퍼런스 객체를 생성합니다. 이때 KClass#functions를 호출하여 원하는 객체 KFunction을 find로 가져옵니다. 그리고 현재 secetMake 함수는 private이므로 Accessible을 true로 만들어줍니다. 그리고 call 메서드를 instance를 파라미터로 넘겨 호출합니다.

val kClass: KClass<Sabarada> = Sabarada::class
val primaryConstructor = kClass.primaryConstructor
val instance = primaryConstructor?.call(5)

val secretMake: KFunction<*>? = kClass.functions.find { it.name == "secretMake" }
secretMake?.isAccessible = true
println("secretMake() = ${secretMake?.call(instance)}")

그렇다면 추가적인 파라미터가 있으면 어떻게 할까요? fun make(second: Int): Int로 확인해보도록 하겠습니다. 바로 call 메서드를 호출할 때 파라미터로 instance와 함께 추가 파라미터를 넘기면 됩니다.

val kClass: KClass<Sabarada> = Sabarada::class
val primaryConstructor = kClass.primaryConstructor
val instance = primaryConstructor?.call(5)

val make: KFunction<*>? = kClass.functions.find { it.name == "make" }
println("make(5) = ${make?.call(instance, 5)}")

리플렉션을 이용한 변수 접근 및 수정

마지막으로 리플렉션을 통해서 변수에 접근하여 값을 가져오는 방법과 수정하는 방법에 대해서 알아보도록 하겠습니다. 변수를 가져올때 또한 마찬가지로 KClass에서 시작합니다. 이번에는 functions가 아닌 memberProperties를 통해서 가져올 수 있습니다. 가져올때는 일반적으로 find를 이용합니다.

아래의 코드를 보도록 하겠습니다. 아래의 코드들은 리플렉션을 통해 변수에 접근하여 그 값을 읽어오는 코드입니다. functions와 동일한 프로세스로 valvar의 차이 없이 가져올 수 있는 것을 확인하실 수 있습니다.

val kClass: KClass<Sabarada> = Sabarada::class
val primaryConstructor = kClass.primaryConstructor
val instance = primaryConstructor?.call(5)

val immutableSecret = kClass.memberProperties.find { it.name == "immutableSecret" }
immutableSecret?.isAccessible = true
println("immutableSecret = ${immutableSecret?.get(instance!!)}") // immutableSecret = 2

val mutableSecret = kClass.memberProperties.find { it.name == "mutableSecret" }
mutableSecret?.isAccessible = true
println("mutableSecret = ${mutableSecret?.get(instance!!)}") // mutableSecret = 3

하지만 set의 경우 valvar는 차이가 있습니다. val의 경우 리플렉션을 이용하더라도 값을 수정할 수 없습니다. valKProperty이며 var는 KMutableProperty로 레퍼런스 클래스가 다릅니다. 그리고 값을 수정하는 setterKMutableProperty 에서만 지원하고 있습니다. var의 경우 값을 수정하는 방법은 아래와 같습니다. if 및 is를 통해 스마트 캐스팅을 한 후 setter를 가져와 그 메서드를 호출하면됩니다.

val kClass: KClass<Sabarada> = Sabarada::class
val primaryConstructor = kClass.primaryConstructor
val instance = primaryConstructor?.call(5)

val mutableSecret = kClass.memberProperties.find { it.name == "mutableSecret" }
mutableSecret?.isAccessible = true
println("mutableSecret = ${mutableSecret?.get(instance!!)}")

if (mutableSecret is KMutableProperty1) {
    mutableSecret.setter.call(instance, 10)
    println("mutableSecret = ${mutableSecret.get(instance!!)}")
}

그렇다면 코틀린에서 val의 값은 못바꾸는 것인가요? 그렇습니다. 안타깝게도 val의 값을 수정하려면 코틀린의 힘만으로는 불가능합니다. 그래서 자바 리플렉션을 이용해서 값을 바꿔주어야합니다.

마무리

오늘은 이렇게 코틀린의 리플렉션에 대해서 알아보는 시간을 가져보았습니다.

감사합니다.

참조

kotlinlang_reflection

baeldung_reflection

stackoverflow_how-to-change-a-kotlin-private-val-using-reflection

chercher.tech_kclass-kotlin-reflection

댓글