본문 바로가기
코틀린

안드로이트 코틀린 Reflection(리플렉션) 기초 정의

by 기계공학 주인장 2023. 3. 9.
반응형

코틀린에서 Reflection이란

런타임에 프로그램의 클래스를 조사하기 위해서 사용되는 기술입니다.

 

즉, 프로그램이 실행중일 때 인스턴스 등을 통해 객체의 내부 구조 등을 파악할 수 있습니다.

 

대표적으로 어노테이션이 그 예입니다.

 

하지만, 함수를 호출하거나 객체를 만들 때마다 조사를 해야 하기 때문에 남용하면

 

성능 저하를 유발할 수 있습니다.

 


Reflection(리플렉션)을 사용하기 위한 의존성(라이브러리) 추가

코틀린에서 Reflection을 사용하기 위해선 아래와 같은 의존성을 추가해야 합니다.

 

안드로이드 스튜디오를 사용하는 경우

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

 

InteliJ IDEA를 사용하고 있는 경우에는 이미 IDE 내에 해당 라이브러리가 있기 때문에

 

따로 추가하지 않아도 괜찮습니다.


Reflection(리플렉션) 사용을 위한 커스텀 어노테이션 클래스 생성

아래와 같이 Reflection에서 사용할 어노테이션을 적용한 클래스를 만들어준다.

(사실 이 부분은 없어도 잘 작동합니다만, 어노테이션에서 리플렉션을 많이 사용하기 때문에 하나의 예시입니다.)

// 커스텀 어노테이션 생성
annotation class CustomAnnotation

@CustomAnnotation
// 주 생성자 1개를 갖고 있는 클래스
class MyClass(val first: Int) {
    // 클래스 멤버변수 2개
    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
    }
}

Reflection(리플렉션)에 접근

코틀린에서 리플렉션을 이용하기 위해서 사용하는 객체를 레퍼런스 객체라고 합니다. 

이러한 레퍼런스 객체를 만드는 방법은 인스턴스를 활용하는 방법과 클래스를 이용하는 방법이 있습니다.

 

다음 코드는 ::class를 사용하여 KClass를 반환받아서

레퍼런스 객체에 접근하는 방법입니다.

val myClass = MyClass(5)
// KClass는 제네릭 타입이기 때문에 out으로 상속에 대한 처리를 해줘야한다.
val kClass: KClass<out MyClass> = myClass::class
참고로 ::class와 ::class.java는 다릅니다.
::class는 코틀린에서 사용되는 KClass를 반환하고
::class.java는 자바에서 사용되는 Class로 반환해 줍니다.

 

인스턴스 객체 생성을 생략하고 다음과 같이 만들 수도 있습니다.

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

이렇게 만든 KClass 타입의 Reflection 레퍼런스 객체로 다음과 같은 정보를 확인할 수 있습니다.

 

val myClass = MyClass(5)
// KClass는 제네릭 타입이기 때문에 out으로 상속에 대한 처리를 해줘야한다.
val kClass: KClass<out MyClass> = myClass::class

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

코틀린 리플렉션(Reflection)을 사용한 객체 생성

리플렉션 객체 생성에서 클래스는 두 가지 종류가 있습니다.

  1. 기본 생성자가 있는 경우
  2. 기본 생성자가 없는 경우

기본 생성자가 있는 경우 리플렉션을 사용한 객체 생성

기본 생성자가 있는데 그냥 리플렉션으로 객체를 생성하려고 하면 에러가 발생합니다.

 

어떤 식으로 에러가 발생하는지 다음 예제를 살펴보겠습니다.

 

참고로 리플렉션의 객체는 createInstance 메서드를 통해 다음과 같이 만들 수 있습니다.

 

fun main() {
    val myClass = MyClass(5)
    // KClass는 제네릭 타입이기 때문에 out으로 상속에 대한 처리를 해줘야한다.
    val kClass: KClass<out MyClass> = myClass::class

    val instance = kClass.createInstance()
    // Error: Exception in thread "main" java.lang.IllegalArgumentException: Class should have a single no-arg constructor: class MyClass
    println("instance = $instance")
}

MyClass는 first라는 주 생성자를 갖고 있는데 리플렉션을 만들려고 했기 때문에 

 

IllegalArgumentException 에러가 발생합니다.

 

그래서 다음과 같이 call 메서드를 사용하여 primaryConstructor를 따로 부여하여 만들 수 있습니다.

fun main() {
    // KClass는 제네릭 타입이기 때문에 out으로 상속에 대한 처리를 해줘야한다.
    val kClass: KClass<MyClass> = MyClass::class
    val primaryConstructor = kClass.primaryConstructor
    // call로 주생성자를 강제로 부여
    val instance = primaryConstructor?.call(5)

    println("MyClass first = ${instance?.first}") // MyClass first = 5
}

 

기본 생성자가 없는 경우에는 call 메서드 없이 그냥 리플렉션으로 객체를 만들 수 있습니다.

코틀린 리플렉션(Reflection)을 사용한 함수 실행

위에서 생성한 객체를 활용해서 MyClass 안에 있는 함수를 실행해 보겠습니다.

 

참고로 MyClass 안에는 아래의 두 함수가 있습니다.

  1. 일반적인 fun 함수
  2. private 처리된 fun 함수

일반적으로는 1번 함수만 호출할 수 있지만

 

리플렉션으로 인스턴스 객체를 만들면 2번 private 처리된 함수도 호출할 수 있습니다.


리플렉션 객체에서 private 함수 호출하기

위에서 생성한 kClass 객체에서 functions를 사용하면

 

해당 클래스 내에 있는 모든 함수를 가져올 수 있습니다.

 

그래서 다음과 같은 방법으로 private 함수에 접근이 가능합니다.

fun main() {
    val kClass: KClass<MyClass> = MyClass::class
    val primaryConstructor = kClass.primaryConstructor
    val instance = primaryConstructor?.call(5)

    // kClass에 있는 모든 함수를 가져와서 "secretMake라는 이름의 함수를 찾는다.
    val secretMake: KFunction<*>? = kClass.functions.find { it.name == "secretMake" }
    // private 함수이기 때문에 강제로 접근이 가능하도록 설정한다.
    secretMake?.isAccessible = true
    // 호출한다.
    println("secretMake() = ${secretMake?.call(instance)}")
}

리플렉션 객체에서 함수에 파라미터가 있을 경우

그렇다면 함수에 파라미터가 필요한 경우에는 어떻게 할까요?

 

아래와 같이 호출할 수 있습니다.

fun main() {
    val kClass: KClass<MyClass> = MyClass::class
    val primaryConstructor = kClass.primaryConstructor
    val instance = primaryConstructor?.call(5)

    val make: KFunction<*>? = kClass.functions.find { it.name == "make" }
    // call 함수에 인수에 대한 값을 지정해준다.
    println("make(5) = ${make?.call(instance, 5)}")
}

코틀린 리플렉션(Reflection)을 사용한 변수 접근 및 수정

마지막으로 클래스 객체 내에 있는 멤버 프로퍼티에 대한 접근을 알아보겠습니다.

 

멤버 프로퍼티의 경우 val / var에 따라 사용하는 방법이 조금 다릅니다.

 

정확히 말하면 가져올 때는 둘 다 동일한 방법으로 가져올 수 있지만

 

값을 변경하거나 넣을 때 코드가 조금 달라집니다.


멤버 프로퍼티 값 가져오기

멤버 프로퍼티 값은 memberProperties을 사용하여 가져올 수 있습니다.

 

MyClass에는 private 멤버 변수만 있기 때문에 함수와 똑같이 isAccesible을 설정해줘야 할 필요가 있다.

 

아래는 리플렉션을 사용해서 해당 클래스의 멤버 변수를 get 하는 코드입니다.

fun main() {
    val kClass: KClass<MyClass> = MyClass::class
    val primaryConstructor = kClass.primaryConstructor
    val instance = primaryConstructor?.call(5)

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

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

 

 

valKProperty이며 varKMutableProperty로 레퍼런스 클래스가 서로 다릅니다.

 

그렇기 때문에 var 변수를 수정하는 방법은 스마트 캐스팅을 사용하여해 줄 수 있습니다.

fun main() {
    val kClass: KClass<MyClass> = MyClass::class
    val primaryConstructor = kClass.primaryConstructor
    val instance = primaryConstructor?.call(5)

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

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

    // mutableSecret를 KMutableProperty1로 인지시킨다.
    if (mutableSecret is KMutableProperty1) {
        mutableSecret.setter.call(instance, 10)
        // mutableSecret = 10
        println("mutableSecret = ${mutableSecret.get(instance!!)}")
    }
}

val 변수의 경우는 안타깝게도 리플렉션을 사용하더라도 변수를 변경할 수 없습니다....


코틀린 리플렉션(Reflection) 사용 시 주의사항

안드로이드 스튜디오 사용 시 코드 난독화를 할 때가 있는데

 

그중에서 아래와 같이 minifyEnabled를 사용하여 난독화를 할 때가 있다

android {
    ..
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    ..
}

minifyEnabled를 사용하면 내가 코드 상에 정의한 클래스 및 함수들이 무작위 이름으로 바뀌게 된다

(예: a class, fun b 등...)

 

하지만, 리플렉션을 사용한 클래스, 함수는 난독화가 되면 안 되는데

그 이유는 위에서 봤다시피 이름을 통해 구별하기 때문입니다.

 

이럴 경우 minifyEnabled을 사용함과 동시에 proguard를  정의해서 난독화하지 않을 패키지를 지정할 수 있다.

 

위 코드에서 보이다시피 'proguard-rules.pro'라는 파일에 해당 위치들이 저장되어 있는데

 

해당 파일의 위치는 다음과 같다.

 

예를 들어 우리가 data 패키지의 모든 data class들이 리플랙션을 쓴다고 할 때

아래와 같이 proguard-rules.pro에 다음  -keep문을 추가함으로써 패키지에 속한 모든 클래스와 함수들의 난독화를 방지할 수 있다.

-keep class com.sample.activity.data.* { *;}

 

이렇게 한 번에 패키지를 지정하는 방법 말고

@Keep 어노테이션을 사용하면 개별적으로 클래스나 함수의 난독화를 막을 수 있다.

 

@Keep
data class TestData(val blogName: String)
6

요약

  • 코틀린의 리플렉션(Reflection)이란?
  • 리플렉션에 접근하기
  • 리플렉션을 사용하여 객체 생성하기
  • 리플렉션으로 만든 객체의 함수 실행하기
  • 리플렉션으로 만든 객체의 변수에 접근하기
반응형


"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."


댓글