[Java/Kotlin] Reflection을 이용해 Unit Test에서 @Value 값 주입하기

고라니·2023년 1월 29일
1
post-thumbnail

Spring Boot에서 전역 설정값을 관리하기 위해 @Value 어노테이션을 쓰는 경우가 많습니다. 인증/보안 관련 비밀키들이 소스코드에 그대로 노출되는 것을 방지하기 위해 사용하는 경우를 예로 들 수 있습니다.

다들 배포 전에 많은 단위 테스트 코드를 작성하실겁니다. 아시다시피, 단위 테스트는 Spring Context 없이 실행되기 때문에 통합 테스트에 비해 가볍고 빠르다는 장점이 있지요.

보통의 경우, @Value 값 주입을 위해서 아래와 같이 Spring Extension을 사용하거나 Setter를 사용합니다.

@ExtendWith(SpringExtension.class)
@ContextConfiguration(
        initializers = {ConfigDataApplicationContextInitializer.class},
        classes = {TestTarget.class} // 테스트 대상 클래스
)

하지만 Spring Extension을 사용하게 되면 Test Context를 만들기 위해 시간 소요가 발생하게 된다는 단점이 존재합니다. 단순히 @Value 값 주입만을 목적으로 Spring Extension을 사용하면 낭비일 수 있습니다. 또한, Setter가 없는 경우에 테스트만을 위한 setter를 만들기는 애매합니다.

Reflection 기술을 사용하게 되면 Spring Framework의 도움 없이 @Value 값 주입이 가능하기 때문에 이러한 단점들이 생기지 않습니다.

그럼, Java와 Kotlin에서 Reflection API 사용법을 알아보고, 이를 활용해 Unit Test까지 작성하는 것을 살펴보겠습니다.

Reflection

Reflection 기술이란, 동적으로 클래스의 정보에 접근하고 추출하는 기술입니다.

Reflection 기술을 이용하면 개발할 때 많은 편리한 기능을 제공할 수 있게 됩니다. 많이들 아시는 것처럼, Spring Container에서 의존성을 주입할 때 Reflection 기술을 사용합니다. DI를 제외하고도, Hibernate에서 Entity의 setter 없는 field 값을 넣을 때 사용하기도 합니다.

Java Reflection API

Java에서 Reflection API는 default package로 기본 제공됩니다.
API를 사용하기 전에, 간단한 클래스를 만들어 보겠습니다.

public class SimpleClass {
    private String privateProperty = "private-property";
    private final String privateFinalProperty = "private-final-property";

    public String getPrivateProperty() {
        return privateProperty;
    }
    
    public String getPrivateFinalProperty() {
        return privateFinalProperty;
    }
    
    private String privateMethod() {
        return "private-method";
    }
}

이제 다른 파일에 main 메소드를 만들어서 SimpleClass의 메타 정보를 접근하고 조작해보겠습니다. API를 사용할 대상 클래스에 대한 메타 정보를 담는 Class 자료형의 변수를 선언하시면 됩니다.

public class BasicReflection {
    public static void main(String[] args) throws Exception {
        Class<SimpleClass> clazz = SimpleClass.class;

    }
}

clazz 변수를 통해, 해당 클래스의 메타 정보를 불러올 수 있습니다.

System.out.println("clazz.getName() = " + clazz.getName());
System.out.println("clazz.getDeclaredFields() = " + Arrays.toString(clazz.getDeclaredFields()));
System.out.println("clazz.getDeclaredMethods() = " + Arrays.toString(clazz.getDeclaredMethods()));

SimpleClassprivateProperty에도 접근해보겠습니다. 일반적으로 접근제어자가 private인 경우에 getter/setter 없이 불가능하지만, Reflection API를 사용하면 접근제어자를 변경할 수 있습니다.

SimpleClass simpleClassInstance = new SimpleClass();
Field privateProperty = clazz.getDeclaredField("privateProperty");

privateProperty.setAccessible(true);
privateProperty.set(simpleClassInstance, "private property reflected!!");

System.out.println(simpleClassInstance.getPrivateProperty());
// private property reflected!!

여기서 주의해야될 점은, private 접근제어자인 경우 setAccessible을 먼저 호출해주어야 에러없이 set 함수를 호출할 수 있습니다. final 필드인 privateFinalProperty에도 같은 방법으로 시도해보겠습니다.

Field privateFinalProperty = clazz.getDeclaredField("privateFinalProperty");
        
privateFinalProperty.setAccessible(true);
privateFinalProperty.set(simpleClassInstance, "private final property reflected!!");

System.out.println(simpleClassInstance.getPrivateFinalProperty());
// private-final-property
System.out.println(privateFinalProperty.get(simpleClassInstance));
// private final property reflected!!

final 필드인 경우에는 getter를 이용한 결과와 get함수를 이용한 결과가 다른 것을 볼 수 있는데, 이는 해당 클래스가 컴파일 될 때 getter의 리턴 값이 문자열 상수로 대체 되었기 때문입니다.

/* SimpleClass.class */
public String getPrivateFinalProperty() {
	return "private-final-property";
}

클래스 메소드도 비슷한 방식으로 접근할 수 있습니다.

Method privateMethod = clazz.getDeclaredMethod("privateMethod");
privateMethod.setAccessible(true);
        
System.out.println(privateMethod.invoke(simpleClassInstance));
// private-method

해당 함수에 넣어줄 파라미터는 invoke 함수에서 객체 뒤에 순차적으로 넣어주면 됩니다.

그 외에도 많은 API들이 있고 자세한 사항은 JAVA Reflection API 공식문서를 보기 권장합니다.

Kotlin Reflection API

Kotlin Reflection API도 기본 제공되며, Java와 쓰임이 비슷하기에 설명을 코드로 대체하겠습니다. 다만, Kotlin은 Java와 다르게 val 타입 변수를 바꾸지 못합니다.

class SimpleClass {
    private var privateProperty = "private-property"
    private val privateFinalProperty = "private-final-property"

    fun getPrivateProperty(): String {
        return privateProperty
    }

    fun getPrivateFinalProperty(): String {
        return privateFinalProperty
    }

    private fun privateMethod(): String {
        return "private-method"
    }
}
fun main() {
    val clazz: KClass<SimpleClass> = SimpleClass::class

    println(clazz.jvmName)
    println(clazz.declaredMemberProperties)
    println(clazz.declaredFunctions)

    val simpleClassInstance: SimpleClass = SimpleClass()

    val privateProperty = clazz.memberProperties.find { it.name == "privateProperty" }
    if (privateProperty is KMutableProperty1) {
        privateProperty.isAccessible = true
        privateProperty.setter.call(simpleClassInstance, "property reflected!!")
        println(simpleClassInstance.getPrivateProperty())
        // property reflected!!
    }

    val privateMethod = clazz.memberFunctions.find { it.name == "privateMethod" }
    if (privateMethod is KFunction) {
        privateMethod.isAccessible = true
        println(privateMethod.call(simpleClassInstance))
        // private-method
    }
}

Unit Test 작성

이제 Reflection API를 이용해서 @Value에 값 주입하는 간단한 단위 테스트 코드를 작성해보겠습니다. 테스트 환경은 jUnit5이며, assertj 라이브러리를 이용했습니다.

먼저 @Value로 값 주입받는 클래스를 만듭니다.

@Component
public class ValueInjection {
    @Value("${value}")
    private String value;

    public String getValue() {
        return value;
    }
}

테스트 코드는 아래와 같습니다.

class ValueInjectionWithReflectionTest {
    @Test
    @DisplayName("Reflection")
    public void valueTest() throws Exception {

        Class<ValueInjection> clazz = ValueInjection.class;
        Field valueField = clazz.getDeclaredField("value");

        Constructor<ValueInjection> defaultConstructor = clazz.getConstructor();
        ValueInjection valueInjection = defaultConstructor.newInstance();

        valueField.setAccessible(true);
        valueField.set(valueInjection, "foo");

        String value = valueInjection.getValue();

        assertThat(value).isEqualTo("foo");
    }
}

정말 Reflection 기술을 사용하면 빠를까요? 비교군을 위해 Spring Extension을 활용해서 @Value 주입받는 또 다른 단위 테스트를 작성해보겠습니다.

@ExtendWith(SpringExtension.class)
@ContextConfiguration(
        initializers = {ConfigDataApplicationContextInitializer.class},
        classes = {ValueInjection.class}
)
class ValueInjectionWithSpringExtensionTest {
    @Autowired
    private ValueInjection valueInjection;

    @Test
    @DisplayName("Spring Extension")
    public void valueTest() {
        String value = valueInjection.getValue();

        assertThat(value).isEqualTo("foo");
    }
}

이제 테스트를 실행시켜보겠습니다.

Reflection 기술을 사용하는 것이 훨씬 빠른 것이 보이시나요?

물론, Reflection API는 동적으로 클래스 메타 정보에 접근하기 때문에, 최적화를 할 수 없어 많이 느리다는 단점도 존재합니다. Reflection API를 쓰지 않아도 되는 경우에도 사용한다면 문제가 될 수 있겠지요. 하지만 @Value 값 주입만을 위해 사용하는 것이라면 충분히 고민해볼만한 선택지인 것 같습니다.

0개의 댓글