Android

Reflection [우아한테크코스 5기 AN_베르]

베르_최성훈 2023. 9. 10. 20:00

자동 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 만들기로 돌아오겠습니다!