Java Reflection은 실행 중인 프로그램의 클래스, 인터페이스, 필드, 메서드 등의 정보를 동적으로 분석하고 조작할 수 있는 기능을 제공하는 Java의 API다. Reflection을 사용하면 컴파일 시간에 알려지지 않은 클래스의 정보를 검색하고, 객체를 생성하고, 메서드를 호출하고, 필드에 접근할 수 있다.

Reflection을 사용하여 다음과 같은 작업을 수행할 수 있습니다:

  1. 클래스 정보 가져오기: Reflection을 사용하면 클래스의 이름, 상위 클래스, 인터페이스, 필드, 메서드 등의 정보를 가져올 수 있다.

  2. 객체 생성하기: Reflection을 사용하여 동적으로 클래스의 인스턴스를 생성할 수 있다. 이는 프로그램 실행 중에 클래스를 동적으로 로드하고 인스턴스를 생성하는 데 사용될 수 있다.

  3. 메서드 호출하기: Reflection을 사용하여 클래스의 메서드를 호출할 수 있다. 메서드 이름, 매개변수 타입 및 인수를 동적으로 지정할 수 있다.

  4. 필드에 접근하기: Reflection을 사용하여 클래스의 필드에 접근하고 값을 설정하거나 가져올 수 있습니다. 필드의 이름을 동적으로 지정하여 필드를 조작할 수 있다.

Reflection 예시

Class 객체 가져오기

Class<MyClass> myClass = MyClass.class;
String className = "com.example.MyClass";
Class<?> myClass = Class.forName(className);
MyClass myObject = new MyClass();
Class<? extends MyClass> myClass = myObject.getClass();

Class 정보 가져오기

import java.lang.reflect.*;

public class ReflectionExample {
    public static void main(String[] args) throws Exception {
        Class<?> myClass = MyClass.class;

        // 클래스 이름 가져오기
        String className = myClass.getName();
        System.out.println("Class Name: " + className);

        // 상위 클래스 가져오기
        Class<?> superClass = myClass.getSuperclass();
        System.out.println("Superclass: " + superClass.getName());

        // 인터페이스 가져오기
        Class<?>[] interfaces = myClass.getInterfaces();
        System.out.println("Interfaces:");
        for (Class<?> iface : interfaces) {
            System.out.println("- " + iface.getName());
        }

        // 모든 메서드 가져오기
        Method[] methods = myClass.getMethods();
        System.out.println("Methods:");
        for (Method method : methods) {
            System.out.println("- " + method.getName());
        }

        // 모든 필드 가져오기
        Field[] fields = myClass.getFields();
        System.out.println("Fields:");
        for (Field field : fields) {
            System.out.println("- " + field.getName());
        }

        // 생성자 가져오기
        Constructor<?>[] constructors = myClass.getConstructors();
        System.out.println("Constructors:");
        for (Constructor<?> constructor : constructors) {
            System.out.println("- " + constructor.getName());
        }
    }
}

생성자 사용하기

import java.lang.reflect.*;

public class ReflectionConstructorExample {
    public static void main(String[] args) throws Exception {
        Class<?> myClass = MyClass.class;

        // 매개변수가 없는 생성자 호출
        Constructor<?> constructor1 = myClass.getConstructor();
        Object instance1 = constructor1.newInstance();
        System.out.println(instance1);

        // 매개변수가 있는 생성자 호출
        Constructor<?> constructor2 = myClass.getConstructor(String.class, int.class);
        Object instance2 = constructor2.newInstance("Example", 42);
        System.out.println(instance2);
    }
}

매개변수가 있는 경우, 매개변수 타입 클래스를 순선대로 전달한다. 생성자를 가져왔다면 newInstance 메서드를 사용해서 생성자를 호출한다.

Field 조작하기

import java.lang.reflect.*;

public class ReflectionFieldExample {
    public static void main(String[] args) throws Exception {
        Class<?> myClass = MyClass.class;

        // 필드에 값 설정
        Field field = myClass.getField("myField");
        Object instance = myClass.newInstance();
        field.set(instance, 42);

        // 필드 값 가져오기
        int fieldValue = (int) field.get(instance);
        System.out.println("Field value: " + fieldValue);
    }
}

getField() 메서드로 필드정보를 가져온 뒤, set() 메서드를 호출하면서 필드가 속한 인스턴스와 설정할 값을 전달한다. 필드 값을 가져올 때는 get() 메서드를 사용한다.

💡 Field와 Method 정보를 가져올 때 getField() 또는 getMethod() 메소드를 사용한다. 이 외에도 getDeclaredFields(), getDeclaredMethods(), getDeclaredAnnotations() 등이 있는데 앞에 declared가 붙은 메소드의 경우 상속받은 클래스와 인터페이스를 제외한 해당 클래스에서 직접 정의된 내용만 가져온다.

Method 다루기

import java.lang.reflect.*;

public class ReflectionMethodExample {
    public static void main(String[] args) throws Exception {
        Class<?> myClass = MyClass.class;

        // 매개변수가 없는 메서드 호출
        Method method1 = myClass.getMethod("myMethod");
        Object instance1 = myClass.newInstance();
        method1.invoke(instance1);

        // 매개변수가 있는 메서드 호출
        Method method2 = myClass.getMethod("myMethod", String.class, int.class);
        Object instance2 = myClass.newInstance();
        method2.invoke(instance2, "Hello, Reflection!", 42);
    }
}

위 예시에서는 MyClass 클래스의 메서드를 Reflection을 사용하여 호출한다. getMethod() 메서드를 사용하여 메서드 객체를 가져온 후, invoke() 메서드를 호출하여 메서드를 실행한다. 첫 번째 예시에서는 매개변수가 없는 myMethod 메서드를 호출한다. getMethod() 메서드에 메서드 이름을 전달한다. 두 번째 예시에서는 **String**과 int 타입의 매개변수를 갖는 myMethod 메서드를 호출한다. getMethod() 메서드에 메서드 이름과 해당 매개변수 타입들을 순서대로 전달한다. 각각의 invoke() 메서드 호출은 첫 번째 인수로 메서드가 속한 객체를 전달하고, 두 번째 인수부터는 메서드의 매개변수들을 전달한다.

Reflection을 사용하여 메서드를 호출할 때는 예외 처리에 유의해야 한다. NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException 등의 예외가 발생할 수 있으므로 적절히 처리해야 한다.

또한 메서드가 private이거나 protected인 경우에는 getDeclaredMethod() 메서드를 사용하고, 필요에 따라 **setAccessible(true)**를 호출하여 접근 가능하게 설정해야 한다.

어노테이션(Annotations)

import java.lang.annotation.*;
import java.lang.reflect.*;

// 예시로 사용할 어노테이션 정의
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface MyAnnotation {
    String value();
}

public class ReflectionAnnotationExample {
    // 어노테이션이 적용된 메서드
    @MyAnnotation("Hello, Annotation!")
    public static void myMethod() {
        System.out.println("Inside myMethod");
    }

    public static void main(String[] args) throws Exception {

        // 어노테이션 가져오기
        MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);

        // 어노테이션 값 사용
        if (annotation != null) {
            String value = annotation.value();
            System.out.println("Annotation value: " + value);
        }
    }
}

위의 예시에서는 **MyAnnotation**이라는 어노테이션을 정의하고, ReflectionAnnotationExample 클래스의 myMethod 메서드에 어노테이션을 적용했다. getAnnotation() 메서드를 사용하여 해당 메서드에 적용된 어노테이션 객체를 가져온다. MyAnnotation 타입의 어노테이션이 적용되어 있으므로, **getAnnotation(MyAnnotation.class)**을 호출합니다. 반환된 어노테이션 객체를 사용하여 어노테이션의 값을 가져와서 출력합니다.

Reflection을 사용하여 어노테이션을 다룰 때에는 적절한 예외 처리를 수행해야 한다. NoSuchMethodException, NoSuchFieldException, SecurityException, IllegalAccessException 등의 예외가 발생할 수 있으므로 적절히 처리해야 한다.

또한, 어노테이션의 유지 정책(Retention)과 대상(Target)에 주의해야 한다. 예시에서는 **@Retention(RetentionPolicy.RUNTIME)**과 **@Target(ElementType.METHOD)**을 사용하여 어노테이션을 실행 시간에 유지하고 메서드에 적용하도록 정의했다.

Reflection이 사용되는 경우

  • 런타임에 동적으로 클래스를 로드하고 인스턴스를 생성해야 하는 경우.

  • 애플리케이션에서 외부 구성 파일로부터 클래스 이름을 동적으로 읽어와야 하는 경우.

  • 프레임워크 또는 라이브러리에서 일반화된 방법으로 객체를 검사, 조작 또는 확장해야 하는 경우.

  • 디버깅 도구 또는 테스트 도구에서 객체의 정보를 동적으로 분석하고 조작해야 하는 경우.

Reflection은 강력하고 유연한 기능을 제공하지만, 오용하면 성능 저하, 유지 보수의 어려움, 타입 안정성의 상실 등의 문제가 발생할 수 있다. 일반적으로 메소드를 호출한다면, 컴파일 시점에 분석된 클래스를 사용하지만 리플렉션은 런타임 시점에 클래스를 분석하므로 속도가 느리다. 따라서 컴파일 시점에 타입 체크도 불가능하며 객체의 추상화가 깨지는 문제가 있다.

Reference

https://hudi.blog/java-reflection/