Reflection [우아한테크코스 5기 AN_베르]
자동 DI 블로그 글을 쓰다가 리플렉션에 대한 정리를 먼저 하고 넘어가는게 좋다고 판단되어 주제로 선정하였다.
자동 DI 를 구현하려면 리플렉션을 먼저 알아야한다.
Reflection 이 뭐지?
kotlin in Action 에 따르면 실행 시점에 동적으로 객체의 프로퍼티와 메서드에 접근할 수 있게 해주는 방법이다.
이는 성능을 떨어뜨리며 객체지향을 무시해버리기 때문에 개발할 때 보통 사용하지 않는 방법이다. (아래 사용 방법을 보면 이해할 것이다.) 하지만 라이브러리 등을 구현하려면 꼭 필요한 경우도 존재한다.
코틀린에서는 두 가지 서로 다른 Reflection API 를 사용할 수 있는데 자바 Reflection, 코틀린 Reflection 이 있다.
코틀린 클래스는 자바 바이트코드로 컴파일 되기 때문에 자바 Reflection API 도 코틀린 클래스를 컴파일한 바이트코드를 완벽히 지원한다.
이는 Reflection을 사용하는 자바 라이브러리와 코틀린 코드가 완벽 호환된다는 뜻이므로 중요하다.
코틀린 Reflection API 는 자바에는 없는 프로퍼티나 널이 될 수 있는 타입과 같은 코틀린 고유의 개념에 대한 리플렉션을 제공한다. KClass, KCallable, KFunction, KProperty 를 알고 사용해야한다.
Reflection 은 매우 복잡하기 때문에 하나하나 알아보기 보다는 어떻게 사용하면 되는지 알아보자
간단한 테스트로 알아보자
class Person(
var firstName: String,
val lastName: String,
private var age: Int,
private val hobby: String,
) {
fun getAge(): Int = age
fun getHobby(): Int = age
}
먼저 Person 이라는 객체를 정의한다. 가변 firstName, 읽기 전용 lastName, 비공개 가변 age 세가지 프로퍼티를 가진다. 비공개 읽기 전용 hobby 네가지 프로퍼티를 가진다.
1. Public Var Property
@Test
fun `Change value of Public Var Property `() {
// given
val person = Person("Ber", "Choi", 25, "singing")
// when
Person::firstName.set(person, "SeongHoon")
// then
assertThat(person.firstName).isEqualTo("SeongHoon")
}
리플렉션을 사용하면 런타임에 객체에 접근해 값을 변경할 수 있다. Person 의 firstName 은 variable 이므로 set 을 호출할 수 있다.
2. Public Val Property
@Test
fun `공개 읽기 전용 프로퍼티`() {
// given
val person = Person("Ber", "Choi", 25, "singing")
// when
val lastNameField = Person::class.java.getDeclaredField("lastName")
lastNameField.apply {
isAccessible = true
set(person, "Park")
}
// then
assertThat(person.lastName).isEqualTo("Park")
}
Person 의 lastName 은 읽기 전용이라 코틀린에서 set 을 호출할 수 없다. 따라서 java API 를 사용해 필드에 접근해야 한다. getDeclaredField 를 사용하고 isAccessible 을 true 로 변경해 접근 가능하도록 하고 set 을 호출했다.
잠깐! DeclaredMemberProperties
@Test
fun `DeclaredProperties in Class`() {
// given & when
val declaredMemberProperties = Person::class.declaredMemberProperties
// then
assertThat(declaredMemberProperties.size).isEqualTo(4)
}
kotlin KClass 의 declaredMemberProperties 로 선언한 프로퍼티들을 가져올 수 있다.
이때 gradle 에 의존성 추가가 필요하다. implementation
(kotlin("reflect"))
@Test
fun `Mutable Properties in DeclaredMemberProperties`() {
// given & when
val mutableProperties = Person::class
.declaredMemberProperties
.filterIsInstance<KMutableProperty<*>>()
// then
assertThat(mutableProperties.size).isEqualTo(2)
}
위와 같이 filterIsInstance 로 가변 프로퍼티 만을 가져올 수도 있다.
리플렉션을 사용하면 private 프로퍼티의 값도 변경할 수 있는데
3. Private Var Property
@Test
fun `Set Value Private Mutable Property`() {
// given
val person = Person("SeongHoon", "Choi", 25, "singing")
// when
val ageProperty = Person::class
.declaredMemberProperties
.filterIsInstance<KMutableProperty<*>>()
.first { it.name == "age" }
ageProperty.apply {
isAccessible = true
ageProperty.setter.call(person, 23)
}
// then
assertThat(person.getAge()).isEqualTo(23)
}
위와 같이 가변 프로퍼티를 필터링으로 가져와서 setter 를 call 한다.
call 함수는 말 그대로 호출하는 것인데 KCallable 을 구현한 객체에 사용 가능하다.
KCallable 은 함수와 프로퍼티를 아우르는 공통 상위 인터페이스로 프로퍼티의 게터를 호출하거나 함수를 호출하거나 하는데 사용된다.
4. Private Val Property
@Test
fun `Set Value Private Immutable Property`() {
// given
val person = Person("SeongHoon", "Choi", 25, "singing")
// when
val ageProperty = Person::class
.declaredMemberProperties
.filterIsInstance<KProperty<*>>()
.first { it.name == "hobby" }
ageProperty.javaField?.apply {
isAccessible = true
set(person, "dancing")
}
// then
assertThat(person.getHobby()).isEqualTo("dancing")
}
마찬가지로 KProperty 에는 javaField 라는 프로퍼티가 존재하는데 이를 사용하면 필드에 접근할 수 있고 위에서 했던 방법과 같이 접근해서 변경할 수 있다.
다음엔 자동 DI 만들기로 돌아오겠습니다!