JAVA/Reflection

Reflection 개요

최-코드 2024. 11. 23. 11:36

Reflection

  • 언어이자, JVM 기능으로 런타임 때 클래스와 객체 정보를 추출할 수 있게 해준다.
  • 이는 Reflection API를 통해 이뤄지는데, JDK에 달린 클래스 집합과 메소드이다.
  • 런타임 동안 다양한 소프트웨어 컴포넌트를 연결하고, 소스코드를 수정하지 않고 새로운 프로그램 순서를 만들 수 있다.
  • 또한 다목적 알고리즘을 만들 수 있다. 실행하고 있는 클래스와 객체에 따라 알고리즘을 쉽게 조정하거나 변경할 수 있다.
  • reflection을 도입하면 입력한 데이터와, 코드를 입력값으로 보고 출력값을 출력한다. reflection이 없는 애플리케이션은 입력한 데이터만을 입력값으로 본다.
  • 프로그램을 실행하면서 객체와 클래스를 분석하고 객체와 클래스를 입력값으로 사용할 수 있어서 뛰어난 소프트웨어를 설계할 수 있다.

Reflection 사용 예제

  • JUnit
    • reflection이 없었으면 main 메서드로 새로운 프로그램을 만들고 test 클래스를 인스턴스화하고 test 메소드 각각을 수동으로 설정하고 호출해야한다.
    • 하지만 @Before와 @Test 어노테이션을 통해 JUnit이 새로운 프로그램을 만들고, test 클래스를 인스턴스화해준다. 또한 어노테이션이 붙은 메서드를 전부 찾고, 순서에 맞게 실행한다.
  • Spring 의존성 주입
    • @Autowired를 사용하면 @Bean 어노테이션이 붙은 클래스를 찾고, 객체로 만들어준 후 그 객체를 주입해준다.
  • Jackson
    • 입력값이 문자열이라면, reflection을 사용해서 클래스를 확인하고, 필드를 전부 분석한다. 이후 필드명에 따라 json 문자열에서 가져온 값을 입력한다.

 

Reflection API 진입점

  • 클래스와 객체를 확인할 수 있도록 Reflection 로직을 작성할 수 있는 Class<?> 객체이다.
Reflection 로직은 클래스나 객체의 정보를 추출하는 것을 말한다.
  • Class<?> 타입 객체엔 어떤 메소드와 필드를 포함하는지, 어떤 2클래스를 확장하고, 어떤 인터페이스를 실행하는지 알 수 있다.
  • Class<?> 타입 객체를 얻는 방법은 아래와 같다.
    • 객체.getClass()
      • 인터페이스 타입의 변수를 선언하고 특정 클래스를 할당했을 때 이 때의 getClass의 타입은 런타임 시점의 타입이 된다.
      • 예를 들어 Map<String, Integer> map = new HashMap<>();일 때 HashMap에 대한 Class<?> 타입 객체를 얻는다.
      • 원시타입은 객체가 아니기에 Class<> 타입 객체를 얻을 수 없다.
    • 타입이름.class
      • 클래스의 인스턴스가 없을 때 사용한다.
      • 원시타입에도 사용할 수 있다. 예를 들어 double.class와 같이 사용가능하다.
    • Class.forName(..)
      • 패키지명을 포함한 이름을 사용해서 클래스를 찾는 정적 메소드이다.
      • 위의 두 방식과 다르게 런타임 시점에 오류를 발견한다. 따라서 세 가지 방법중 가장 위험한 방법이다.
      • 하지만 사용자 정의 구성파일에서 전달할 때 유용하다. 외부 텍스트 파일만 수정하면 되므로, 소스 코드를 변경하거나 다시 컴파일 하지 않아도 된다.
      • 클래스 내에 내부 클래스에 접근할 때는 $을 이용하면 된다.
      • private이어도 가져올 수 있다.

Example

public class Main {

    public static void main(String[] args) throws ClassNotFoundException {
        Class<String> stringClass = String.class;

        Map<String, Integer> mapObject = new HashMap<>();

        Class<?> hashMapClass = mapObject.getClass();

        Class<?> sqaureClass = Class.forName("exercises.Main$Square"); // 내부 클래스 접근

//        printClassInfo(stringClass, hashMapClass, sqaureClass);

//      익명 클래스
        Drawable circleObject = new Drawable() {
            @Override
            public int getNumberOfCorners() {
                return 0;
            }
        };

        printClassInfo(Collection.class, boolean.class, int [][].class, Color.class, circleObject.getClass());
    }

    private static void printClassInfo(Class<?>... classes) {
        for (Class<?> clazz : classes) {
            System.out.printf("class name : %s, class package name : %s%n",
                    clazz.getSimpleName(),
                    clazz.getPackageName());

            Class<?>[] implementedInterfaces = clazz.getInterfaces();

            for (Class<?> implementedInterface : implementedInterfaces) {
                System.out.printf("class %s implement : %s%n",
                        clazz.getSimpleName(),
                        implementedInterface.getSimpleName());
            }

            System.out.println("Is array : " + clazz.isArray());
            System.out.println("Is primitive : " + clazz.isPrimitive());
            System.out.println("Is enum : " + clazz.isEnum());
            System.out.println("Is interface : " + clazz.isInterface());
            System.out.println("Is anonymous : " + clazz.isAnonymousClass());

            System.out.println();
            System.out.println();
        }
    }

    private static class Square implements Drawable {

        @Override
        public int getNumberOfCorners() {
            return 4;
        }
    }

    private interface Drawable {
        int getNumberOfCorners();
    }

    private enum Color {
        BLUE,
        RED,
        GREEN
    }
}

cf) inner 클래스는 static으로 선언해야 한다. inner 클래스를 객체로 만들기 위해서는 outer 클래스를 초기화한뒤 inner 클래스를 초기화해야 한다. 이러한 단계 때문에 inner 클래스가 outer 클래스를 참조하지 않아도 생성자에서 숨겨진 외부 참조가 생성된다. 이 때문에 outer 클래스가 GC 대상에서 빠져서 메모리 관리가 안 된다.