[Kotlin] Reflection이란 무엇일까?

그놈의 Reflection! Reflection 완전 정복하러 가기

Anonymous
7 min readApr 16, 2023
Photo by Ivana Cajina on Unsplash

면접 준비를 하는 과정에서 Reflection이라는 단어를 정말 많이 봤다. 그 당시에는 Reflection에 대해 구체적으로 공부할 시간이 많지 않았기 때문에 간단히 객체를 분석(?)한다고만 알고 있었다.

하지만, 최근에 프로젝트 코드를 분석하는 과정에서 KClass을 봤고 이 Class는 Reflection과 연결이 되어있다는 느낌을 받았다(조사를 해봤을 때!). 그렇기 때문에 Reflection을 Kotlin in Action 책을 통해 공부하고자 다짐했다.

Reflection

실행 시점(동적으로) 객체의 Property와 Method에 접근할 수 있는 방법을 의미한다. 보통 객체의 Property와 Method에 접근할 때는 컴파일 과정에서 찾아내 해당 객체가 존재함을 보장한다. 하지만, 컴파일 과정이 아닌 런타임 과정에서만 객체의 Property와 Method를 알 수 있거나 타입과 관계없이 객체를 다뤄야 하는 경우가 있다. 예를 들어 Json 직렬화 라이브러리가 있다. 이는 실행이 되기 전까지는 직렬화할 Property와 Method에 대한 정보는 알 수 없으며 어떤 객체든지 간에 Json으로 변환할 수 있어야 한다.

Reflection Library

Reflection을 할 수 있는 Library는 크게 두 가지 java.lang.reflect packagekotlin.reflect package가 존재한다. Kotlin Class의 경우에는 컴파일 과정에서 자바 바이트 코드로 변환이 되기 때문에 Kotlin에서 java package를 사용해도 완전히 호환이 된다.

그렇다면 두 Library에는 차이가 있을까? 물론이다! kotlin.reflect package 같은 경우에는 java에 존재하지 않는 Property나 Nullable한 타입에 대한 Reflection을 제공한다고 할 수 있다. 이 외에도 복잡한 기능은 제공하지 않는다고 하는데 정확한 건 잘 모르겠다 ㅎㅎ;;

Kotlin Reflection API

KClass: Class를 표현하는 것으로 이를 통해 Class안에 모든 선언을 열거하고 접근할 수 있다. MyClass::class를 통해서 KClass의 객체를 얻을 수 있다. Android에서 startActivity를 통해 원하는 Activity로 이동할 때 다음과 같은 코드를 본적이 있을 것이다.

val intent = Intent(this, MainActivity::class.java)
startActivity(intent)

여기서 MainActivity::class를 통해 KClass의 객체를 획득하고 .java를 통해 java의 Class로 변환한다.

그렇다면 KClass의 내부는 어떻게 구성이 되어있을까? KClass는 우선 Interface이며 내부에는 다양한 Property가 존재한다. 대표적인 Property에 대해 소개하자면 다음과 같다.

interface KClass<T: Any> {
val simpleName: String?
val qualifiedName: String?
val members: Collection<KCallable<*>>
// KClass<*>: 어떤 Class 레퍼런스도 대입할 수 있다.
val constructors: Collection<KFunction<T>>
val nestedClasses: Collection<KClass<*>>
...

}

kCallable: 여기서 members는 각 Property와 Function에 대한 Collection이며 Property와 Function의 공통 상위 Interface이기 때문에 members의 타입이 위와 같이 형성될 수 있다.

interface KCallable<out R> {
fun call(vararg args: Any?): R
...
}

위의 interface는 KCallable의 내부를 알 수 있다. 가장 중요한 사실은 call이라는 function이 존재하며 varag 리스트로 함수 인자를 전달하고 Reflection을 통해서 call을 통해 특정 인자에 해당하는 함수 내 행위를 호출할 수 있다.

fun foo(x: Int) = println(x)
val kFunction = ::foo
println(kFunction.call(42)) // 42 출력

kFunctionN: kCallable.call method같은 경우는 인자를 넘겨야 하며 인자의 개수와 파라미터의 개수가 일치해야 한다. 만약 위의 코드에서 인자가 x만 있는데 2개의 파라미터를 제공한다면 Runtime Exception이 발생할 것이다.

Runtime Exception을 예방하고 파라미터와 인자의 개수를 일치시키기 위해서 kFunctionN을 타입으로 지정해줄 수 있다. kFunctionN에서 N은 인자의 개수이며 만약 N=1일 경우 kFunction1<Int, Unit>과 같은 타입으로 지정해줄 수 있다. 더 확실한 이해를 위해 예시를 들겠다.

fun sum (x: Int, y: Int) = x + y
val kFunction : KFunction2<Int, Int, Int> = ::sum
println(kFunction.invoke(1, 2) + kFunction(3, 4) // 10
// invoke를 사용하면 인자의 개수나 타입이 맞지 않는 경우 컴파일이 불가능하다.
kFunction(1) // Error

위와 같이 N=2이기 때문에 인자가 2개라는 의미를 가지며 2개일 때는 function의 return값이 결정되지만 인자가 1개인 경우 Error가 발생한다.

KPropertyN: 런타임 과정에서 reflection을 통해 property를 가져올 수 있다. kCallable이 상위 interface이기 때문에 이 역시 call function을 사용할 수 있다. 하지만, KPropertyN는 더 좋은 방법을 제공한다. 바로 get method이다.

최상위 Property(Class 내부가 아닌 외부에 정의하는 Property)는 KProperty0 interface의 인스턴스로 표현되며 인자가 없는 get method가 있다. 멤버 Property는 Property1 interface의 인스턴스로 표현되며 인자가 1개 있는 get method가 있다. 맴버 Property는 어떤 객체에 속해 있는 Property이기 때문에 이를 가져오려면 맴버 Property가 속한 객체를 알아야 한다. 따라서 인자로 맴버 Property가 속한 객체를 넘기게 되고 reflection을 하는 과정에서 동적으로 원하는 값을 가져오게 된다.

class Person(val name: String, val age: Int)
val person = Person("SEHUN", 25)
val memberProperty = Person::age
println(memberProperty.get(person)) // 25

위와 같이 Property에서 get method를 사용하고자 할 때 인자로 맴버 Property가 속한 객체를 넣어줘야 한다.

KProperty를 사용할 때 주의해야할 사항은 최상위 Property와 맴버 Property만 Reflection을 통해 접근이 가능하며 function의 local 변수는 접근할 수 없다.

느낀점

Reflection에 대해 구체적으로 공부해봤다. 평소에는 그저 객체를 분석한다라는 개념만 알고 있었지만 이렇게 깊은 내용이 있는지 몰랐다. 이번 기회를 통해 KClass, KFunction와 같은 것들이 project code에 나왔을 때 빠르게 이해할 수 있다고 생각했다. 사실 Annotation과 Reflection은 많은 관련이 있어보이는데 다음 블로그 피드는 Annotation을 주제로 할 예정이다.

블로그 피드 내용은 틀릴 수 있음을 알려드립니다. 피드백은 언제나 환영입니다!

--

--