리플렉션(Reflection)이란?

리플렉션은 말 그대로 '반사', '반영'의 의미를 가지고있습니다. 구체적인 클래스 타입을 알지 못하더라도 바이트코드를 이용해 해당 클래스의 메소드, 타입, 변수들을 참조하여 값을 찾을 수 있는 JAVA API입니다. 즉 컴파일 시점이 아닌 런타임에 동적으로 특정 클래스의 정보를 추출할 수 있고 변수를 변경하거나 메소드를 호출할 수 있는 프로그래밍 기법입니다. 

코드를 작성할 시점에는 어떤 타입의 클래스를 사용할지 모르지만, 런타임 시점에 지금 실행되고 있는 클래스를 가져와서 실행해야 하는 경우 사용됩니다.  intelliJ의 자동완성 기능, Lombok, Spring Container, Spring annotation 등에서 사용됩니다.

리플렉션은 아래와 같은 정보들을 가져올 수 있습니다.

  • Class
  • Constructor
  • Method
  • Field

위 정보들로 객체를 생성하거나 메소드를 호출하거나 변수에 값을 할당할 수 있습니다.

KClass vs Class

설명에 들어가기 앞서 자바에서 SomeClass.classclass 타입을 반환합니다. 반면에 코틀린에서의 SomeClass::classKClass 타입을 반환합니다. 때문에 KClass 타입을 Class 타입으로 변환해야 하는데, 이 때 .java 를 사용하면 Class 타입을 반환합니다.

fun main() {
    val kClazz: KClass<SomeClass> = SomeClass::class
    val clazz: Class<SomeClass> = SomeClass::class.java
}

KClass 안의 java 프로퍼티의 getter는 위 코드 사진과 같이 확장함수 형태로 되어있습니다. KClass에서의 자바 클래스 타입을 반환하게 됩니다.

예제

open class Animal(
    private val name: String,
    private val city: String = "seoul"
) {
    private fun sleep() {
        println("sleep $name")
    }

    private fun eat(food: String) {
        println("eat: $food")
    }

    private fun walt() {
        println("walk $city")
    }
}

class Dog(private val name: String) : Animal(name) {
    private fun sayName() {
        println("my name: $name")
    }

    private fun sayCity(city: String) {
        println("my city: $city")
    }
}

위 클래스로 간단한 예제를 작성하겠습니다.

 

Class

class를 알고 있을 때

fun main() {
    val dogClass: KClass<Dog> = Dog::class
    println("class name: ${dogClass.simpleName}")
    // class name: Dog
}

class를 알 수 없고 이름만 알고 있을 때

fun main() {
    val clazz = Class.forName("Dog")
    println("class name: ${clazz.simpleName}")
    // class name: Dog
}

 

Constructor

인자가 없는 생성자

fun main() {
    val clazz = Class.forName("Dog")
    val declaredConstructors = clazz.getDeclaredConstructor()

    println(declaredConstructors.name)
    // 매개변수가 없는 생성자가 없기 때문에 NoSuchMethodException 발생
}

특정 매개변수를 가진 생성자

fun main() {
    val clazz = Class.forName("Dog")
    val declaredConstructors = clazz.getDeclaredConstructor(String::class.java) // String 매개변수

    println("name: ${declaredConstructors.name} parameterCount: ${declaredConstructors.parameterCount}")
    // name: Dog parameterCount: 1
}

모든 생성자

fun main() {
    val clazz = Class.forName("Dog")
    val declaredConstructors = clazz.declaredConstructors

    declaredConstructors.map { println("name: ${it.name} count: ${it.parameterCount}") }
    // name: Dog count: 1
}

public 생성자

fun main() {
    val clazz = Class.forName("Dog")
    val declaredConstructors = clazz.constructors

    declaredConstructors.map { println("name: ${it.name} count: ${it.parameterCount}") }
    // name: Dog count: 1
}

 

Method

매개변수가 있는 Method

fun main() {
    val clazz = Class.forName("Dog")
    val method = clazz.getDeclaredMethod("sayCity", String::class.java)

    println("Method: $method")
    // Method: private final void Dog.sayCity(java.lang.String)
}

매개변수가 없는 Method

fun main() {
    val clazz = Class.forName("Dog")
    val method = clazz.getDeclaredMethod("sayName")

    println("Method: $method")
    // Method: private final void Dog.sayName()
}

모든 Method

fun main() {
    val clazz = Class.forName("Dog")
    val methods: Array<Method> = clazz.declaredMethods

    methods.map { println(it) }
    // private final void Dog.sayName()
    // private final void Dog.sayCity(java.lang.String)
}

상속받은 Method와 public Method

fun main() {
    val clazz = Class.forName("Dog")
    val methods: Array<Method> = clazz.methods

    methods.map { println(it) }
    // public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
    // public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
    // public final void java.lang.Object.wait() throws java.lang.InterruptedException
    // public boolean java.lang.Object.equals(java.lang.Object)
    // public java.lang.String java.lang.Object.toString()
    // public native int java.lang.Object.hashCode()
    // public final native java.lang.Class java.lang.Object.getClass()
    // public final native void java.lang.Object.notify()
    // public final native void java.lang.Object.notifyAll()
}

Object 클래스로부터 상속받은 모든 메소드를 확인할 수 있습니다.

 

Field

이름이 일치하는 Field

fun main() {
    val clazz = Class.forName("Dog")
    val field: Field = clazz.getDeclaredField("name")

    println(field)
    // private final java.lang.String Dog.name
}

선언된 모든 Field

fun main() {
    val clazz = Class.forName("Dog")
    val fields: Array<Field> = clazz.declaredFields

    fields.map { println(it) }
    // private final java.lang.String Dog.name
}

 

하지만 상속받은 필드 정보는 가져오지 않습니다.

상속된 모든 Field

fun main() {
    val clazz = Class.forName("Dog")
    val fields: Array<Field> = clazz.fields

    fields.map { println(it) }
}

 

Spring BeanFactory 예제

위에서 본 예제들로 Spring BeanFactory를 간략하게 구현해보고자 합니다. AutoWired 어노테이션이 붙은 클래스의 인스턴스를 만들어서 반환하는 역할을 수행합니다.

class ContainerService {
    companion object {
        fun <T> getObject(classType: Class<T>): T {
            // 기본생성자를 통해서 인스턴스를 만든다.
            val instance = createInstance(classType)
            // 클래스의 모든 필드를 불러온다.
            classType.declaredFields
                // 어노테이션에 AutoWired를 갖는 필드만 필터
                .filter { it.isAnnotationPresent(AutoWired::class.java) }
                .forEach {
                    // 필드의 인스턴스 생성
                    val fieldInstance: Any = createInstance(it.type)
                    // 필드의 접근제어자가 private인 경우 수정가능하게 설정
                    it.isAccessible = true
                    // 인스턴스에 생성된 필드 주입
                    it.set(instance, fieldInstance)
                }
            return instance
        }

        private fun <T> createInstance(classType: Class<T>): T {
            return try {
                // 해당 클래스 타입의 기본생성자로 인스턴스 생성
                classType.getConstructor().newInstance()
            } catch (e: InstantiationException) {
                throw RuntimeException(e)
            } catch (e: IllegalAccessException) {
                throw RuntimeException(e)
            } catch (e: InvocationTargetException) {
                throw RuntimeException(e)
            } catch (e: NoSuchMethodException) {
                throw RuntimeException(e)
            }
        }
    }
}