Skip to content

김현준, 홍은기 리플렉션

hek edited this page Jul 8, 2024 · 1 revision

구체적인 Class Type을 알지 못하더라도 해당 Class의 method, type, variable들에 접근할 수 있도록 해주는 자바 API이며,
컴파일된 바이트 코드를 통해 Runtime에 동적으로 특정 Class의 정보를 추출할 수 있는 프로그래밍 기법이다.

리플렉션 클래스 및 메서드

  • Field 클래스

    • clazz.getDeclaredFields()
    • getType()
      • 이 필드 객체가 나타내는 필드의 클래스 타입을 반환합니다.
    • getGenericType()
      • 이 필드 객체가 나타내는 필드의 제네릭 타입을 반환합니다.
    • getName()
      • 이 필드 객체가 나타내는 필드의 이름을 반환합니다.
    • getModifiers()
      • 이 필드 객체가 나타내는 필드의 접근 수정자를 정수 값으로 반환합니다.
    • isSynthetic()
      • 이 필드가 합성 필드인지 여부를 반환합니다.
    • set(Object obj, Object value)
      • 지정된 객체의 이 필드 값을 value로 설정합니다.
      • 예외: IllegalAccessException, IllegalArgumentException
    • get(Object obj)
      • 지정된 객체의 이 필드 값을 반환합니다.
      • 예외: IllegalAccessException, IllegalArgumentException
  • Constructor 클래스

    • getName()
      • 생성자의 이름을 반환합니다. 일반적으로 클래스의 이름과 동일합니다.
    • getModifiers()
      • 생성자의 접근 제어자 및 기타 수정자를 비트 필드 형식으로 반환합니다. Modifier 클래스의 정적 메서드를 사용하여 이 값을 해석할 수 있습니다.
    • getParameterTypes()
      • 생성자의 파라미터 타입들을 Class<?> 배열로 반환합니다.
    • getExceptionTypes()
      • 생성자가 던질 수 있는 예외 타입들을 Class<?> 배열로 반환합니다.
    • newInstance(Object... initargs)
      • 지정된 인수로 생성자를 호출하여 새로운 인스턴스를 생성합니다. 이 메서드는 생성자가 던질 수 있는 모든 예외를 던질 수 있습니다.
    • isVarArgs()
      • 생성자가 가변 인수(varargs) 파라미터를 사용하는지 여부를 반환합니다.
    • isSynthetic()
      • 생성자가 합성(synthetic) 생성자인지 여부를 반환합니다. 합성 생성자는 컴파일러가 내부 용도로 생성한 생성자를 의미합니다.
    • getDeclaringClass()
      • 이 생성자를 선언한 클래스를 반환합니다.
    • getAnnotatedReturnType()
      • 생성자의 반환 타입에 대한 주석 정보를 포함하는 AnnotatedType 객체를 반환합니다.
    • getParameterAnnotations()
      • 생성자의 각 파라미터에 대한 주석을 2차원 배열로 반환합니다. 각 파라미터는 여러 주석을 가질 수 있습니다.
    • getAnnotatedParameterTypes()
      • 생성자의 각 파라미터 타입에 대한 주석 정보를 포함하는 AnnotatedType 배열을 반환합니다.
  • Method 클래스

    • getName()
      • 메서드의 이름을 반환합니다.
    • getReturnType()
      • 메서드의 반환 타입을 Class<?> 객체로 반환합니다.
    • getParameterTypes()
      • 메서드의 파라미터 타입들을 Class<?> 배열로 반환합니다.
    • getExceptionTypes()
      • 메서드가 던질 수 있는 예외 타입들을 Class<?> 배열로 반환합니다.
    • getModifiers()
      • 메서드의 접근 제어자 및 기타 수정자를 비트 필드 형식으로 반환합니다. Modifier 클래스의 정적 메서드를 사용하여 이 값을 해석할 수 있습니다.
    • invoke(Object obj, Object... args)
      • 지정된 객체에서 이 메서드를 호출합니다. args는 메서드에 전달할 인수입니다.
    • isVarArgs()
      • 메서드가 가변 인수(varargs) 파라미터를 사용하는지 여부를 반환합니다.
    • isBridge()
      • 메서드가 브리지(bridge) 메서드인지 여부를 반환합니다. 브리지 메서드는 제네릭 타입의 형 변환을 처리하기 위해 컴파일러가 생성하는 메서드입니다.
    • isSynthetic()
      • 메서드가 합성(synthetic) 메서드인지 여부를 반환합니다. 합성 메서드는 컴파일러가 내부 용도로 생성한 메서드를 의미합니다.
    • isDefault()
      • 메서드가 인터페이스의 기본 메서드인지 여부를 반환합니다.
    • getDeclaringClass()
      • 이 메서드를 선언한 클래스를 반환합니다.
    • getGenericReturnType()
      • 메서드의 제네릭 반환 타입을 반환합니다.
    • getGenericParameterTypes()
      • 메서드의 제네릭 파라미터 타입들을 반환합니다.
    • getGenericExceptionTypes()
      • 메서드가 던질 수 있는 제네릭 예외 타입들을 반환합니다.
    • getAnnotations()
      • 메서드에 있는 모든 주석을 반환합니다.
    • getDeclaredAnnotations()
      • 메서드에 선언된 모든 주석을 반환합니다.
    • getAnnotation(Class<T> annotationClass)
      • 메서드에 지정된 타입의 주석이 있는지 확인하고, 있다면 반환합니다.
    • getParameterAnnotations()
      • 메서드의 각 파라미터에 대한 주석을 2차원 배열로 반환합니다. 각 파라미터는 여러 주석을 가질 수 있습니다.
    • getAnnotatedReturnType()
      • 메서드의 반환 타입에 대한 주석 정보를 포함하는 AnnotatedType 객체를 반환합니다.
    • getAnnotatedParameterTypes()
      • 메서드의 각 파라미터 타입에 대한 주석 정보를 포함하는 AnnotatedType 배열을 반환합니다.
    • getDefaultValue()
      • 메서드가 애너테이션 타입의 요소인 경우, 해당 요소의 기본 값을 반환합니다.
  • Parameter 클래스

    • boolean equals(Object obj)
      • 이 매개변수가 지정된 객체와 동일한지 여부를 확인합니다.
    • String getName()
      • 이 매개변수의 이름을 반환합니다. (컴파일 옵션에 따라 이름이 저장되지 않을 수 있습니다.)
    • Type getParameterizedType()
      • 이 매개변수의 파라미터화된 타입을 반환합니다. (예: List<String>)
    • int getModifiers()
      • 이 매개변수의 수정자를 int 값으로 반환합니다. (예: final)
    • Class<?> getType()
      • 이 매개변수의 타입을 Class 객체로 반환합니다. (예: String.class)
    • AnnotatedType getAnnotatedType()
      • 이 매개변수의 주석 타입을 반환합니다. (Java 8에서 주석의 타입 정보를 포함하여 반환)
    • <T extends Annotation> T getAnnotation(Class<T> annotationClass)
      • 지정된 주석 타입의 주석을 반환합니다. 주석이 없는 경우 null을 반환합니다
    • Annotation[] getAnnotations()
      • 이 매개변수에 있는 모든 주석을 배열로 반환합니다.
    • Annotation[] getDeclaredAnnotations()
      • 이 매개변수에 선언된 모든 주석을 배열로 반환합니다. 상속된 주석은 포함되지 않습니다.
    • boolean isNamePresent()
      • 이 매개변수의 이름이 런타임에 사용할 수 있는지 여부를 반환합니다.
    • boolean isImplicit()
      • 이 매개변수가 암시적인지 여부를 반환합니다. 암시적 매개변수는 컴파일러가 추가하는 매개변수입니다.
    • boolean isSynthetic()
      • 이 매개변수가 합성인지 여부를 반환합니다. 합성 매개변수는 일반적으로 컴파일러가 생성한 것입니다.
    • boolean isVarArgs()
      • 이 매개변수가 가변 인수(varargs)인지 여부를 반환합니다.

요구사항 1 - 클래스 정보 출력(코드)

@Test
@DisplayName("테스트1: 리플렉션을 이용해서 클래스와 메소드의 정보를 정확하게 출력해야 한다.")
public void showClass() throws Exception {
    SoftAssertions s = new SoftAssertions();
    Class<Question> clazz = Question.class;
    logger.debug("Classs Name {}", clazz.getName());
    constructor();
    method();
    fields();
}

@Test
public void fields() {
    Class<Question> clazz = Question.class;
    for (Field declaredField : clazz.getDeclaredFields()) {
        logger.debug("Field = {} {}", declaredField.getType(), declaredField.getName());
    }
}

@Test
public void constructor() throws Exception {
    Class<Question> clazz = Question.class;
    Constructor[] constructors = clazz.getConstructors();
    for (Constructor constructor : constructors) {
        Parameter[] parameters = constructor.getParameters();
        logger.debug("paramer length : {}", parameters.length);
        for (Parameter parameter : parameters) {
            logger.debug("param type : {} {}", parameter.getType(), parameter.getName());
        }
    }
}

@Test
public void method() {
    Class<Question> clazz = Question.class;
    for (Method declaredMethod : clazz.getDeclaredMethods()) {
        logger.debug("Method = {} {}", declaredMethod.getName(), declaredMethod.getReturnType());

        Class[] parameterTypes = declaredMethod.getParameterTypes();
        logger.debug("paramer length : {}", parameterTypes.length);
        for (Class paramType : parameterTypes) {
            logger.debug("param : {} {}", paramType, paramType.getName());
        }
    }
}
  • showClass()
    • clazz.getName() : 클래스의 이름을 가져오는 메서드
  • fields()
    • getType() : 필드의 타입을 가져온다.
    • getName() : 필드의 이름을 가져온다.
  • constructor()
    • getConstructors() : 생성자 목록을 가져온다.
    • getParameters() : 생성자의 매개변수 목록을 가져온다.

요구사항 2 - test로 시작하는 메소드 실행

@Test
public void runner() throws Exception {
    Class clazz = Junit3Test.class;

    Method[] declaredMethods = clazz.getDeclaredMethods();

    Arrays.stream(declaredMethods)
		        .filter(method -> method.getName().startsWith("test"))
		        .forEach(method -> {
		            try {
		                method.invoke(clazz.newInstance());
		            } catch (Exception e) {
		                throw new RuntimeException(e);
		            }
		        });
}
  • getDeclaredMethods() : 메서드 목록을 가져온다
    • getName() : 메서드 이름을 가져와서 ‘test’로 시작하는지 확인한다.
    • invoke() : 메서드를 실행한다. 첫 번째 인자가 인스턴스, 두 번째부터는 메서드의 매개변수이다.

요구사항 3 - @Test 애노테이션 메소드 실행

@Test
public void run() throws Exception {
    Class clazz = Junit4Test.class;
    for (Method declaredMethod : clazz.getDeclaredMethods()) {
        if(declaredMethod.isAnnotationPresent(MyTest.class)) {
            declaredMethod.invoke(clazz.newInstance());
        }
    }
}
  • isAnnotationPresent() : 메서드에 해당 Annotation이 붙어있는지 확인한다.

요구사항 4 - private field에 값 할당

@Test
@DisplayName("테스트 4 : private field에 값 할당")
public void privateFieldAccess() throws InstantiationException, IllegalAccessException {
    Class<Student> clazz = Student.class;
    logger.debug(clazz.getName());
    Student student = clazz.newInstance();
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
        field.setAccessible(true);
        if (field.getName().equals("name")) {
            field.set(student, "재성");
        } else {
            field.set(student, 20);
        }
    }

    assertThat(student.getName()).isEqualTo("재성");
    assertThat(student.getAge()).isEqualTo(20);
}
  • setAccessible() : true로 설정하면 접근 권한을 무시한다. false일 때는 원래의 접근 제한을 복원한다.
  • set() : field 값을 value로 설정한다.

요구사항 5 - 인자를 가진 생성자의 인스턴스 생성

@Test
@DisplayName("Test 5 : 인자를 가진 인스턴스 생성")
public void constructNew() {
    try {
        // Retrieve the Class object associated with the User class
        Class<?> clazz = User.class;

        // Initialize variables to hold the found constructor and the parameters
        Constructor<?> foundConstructor = null;
        Object[] params = new Object[]{"John Doe", 35};

        // Iterate through all constructors of the User class
        for (Constructor<?> constructor : clazz.getConstructors()) {
            Class<?>[] parameterTypes = constructor.getParameterTypes();

            // Check if the constructor has the correct parameter types
            if (parameterTypes.length == 2
                    && parameterTypes[0] == String.class
                    && parameterTypes[1] == Integer.class) {
                foundConstructor = constructor;
                break;
            }
        }

        // Check if a suitable constructor was found
        if (foundConstructor != null) {
            // Create a new instance of User using the found constructor
            User user = (User) foundConstructor.newInstance(params);

            // Verify the created object
            assertEquals("John Doe", user.getName());
            assertEquals(35, user.getAge());
        } else {
            throw new NoSuchMethodException("No matching constructor found");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

}
  • for (Constructor<?> constructor : clazz.getConstructors())

    이 부분의 코드는 User 클래스의 모든 생성자를 순회하면서 특정한 조건을 만족하는 생성자를 찾는 과정입니다.
    여기서는 생성자가 StringInteger 타입의 두 매개변수를 받아야 한다는 조건을 확인합니다.

  • newInstance() : 매개변수에 Object[]를 넣어도 자동으로 인스턴스가 생성된다.

보너스 미션 1 - 수행시간 측정

@Test
@DisplayName("보너스 미션 1 - 수행시간 측정")
void check_time() throws InstantiationException, IllegalAccessException, InvocationTargetException, InterruptedException {
    Class clazz = Junit4Test.class;
    for (Method declaredMethod : clazz.getDeclaredMethods()) {
        if (declaredMethod.isAnnotationPresent(ElapsedTime.class)) {
            long start = System.currentTimeMillis();
            sleep(10);
            declaredMethod.invoke(clazz.newInstance());
            logger.debug("Time taken: {} ms", System.currentTimeMillis() - start);
        }
    }
}

배운점

홍은기

reflection을 처음 사용해보는데 이를 이용하면 런타임에 동적으로 메타데이터를 가져올 수 있고
이를 이용해서 정확한 테스트 코드를 구현할 수 있을 것 같다는 생각이 들었다.
요구사항 5에서 좀 더 단순한 방식으로 생성자의 형태를 확인할 수 있도록 고민해봐야겠다.

김현준

리플렉션이 많은 부분에서 사용하는데 특히 스프링에서 코드로 가끔 접했는데
이번 기회에 탐색해보면서 이를 이용한 다양한 아이디어를 떠올릴 수 있었다.
이번 미션에서도 실제 사용하고 있는데 더 동적으로 사용할 수 있을 것 같다!

👼 개인 활동을 기록합시다.

개인 활동 페이지

🧑‍🧑‍🧒‍🧒 그룹 활동을 기록합시다.

그룹 활동 페이지

🎤 미니 세미나

미니 세미나

🤔 기술 블로그 활동

기술 블로그 활동

📚 도서를 추천해주세요

추천 도서 목록

🎸 기타

기타 유용한 학습 링크

Clone this wiki locally