Skip to content

김승수, 김현수, 이호석 리플렉션

이호석 edited this page Jul 8, 2024 · 2 revisions

Reflection

  • Reflection은 Class, Constructor, Method, Field영역이 존재하고, 클래스의 인스턴스화 없이 해당 클래스의 정보들을 가져올 수 있다.
  • 컴파일된 클래스 정보를 활용해 동적인 프로그래밍이 가능한 API를 제공한다.



요구사항 1 - 클래스 정보 출력

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

    // field name
    // getDeclaredFields = 필드 접근제어자 + 패키지 + 타입 + 이름
    System.out.println("1. Class.getDeclaredFields() 출력");
    Field[] declaredFields = clazz.getDeclaredFields();
    for (Field declaredField : declaredFields) {
        log.info("declaredField = {}", declaredField);
    }

    System.out.println("============================================================================================================================\n");

    // Field.getName 이름만
    System.out.println("2. Field관련 정보 출력");
    Field[] fields = clazz.getDeclaredFields();

    Arrays.stream(fields)
            .map(Field::getName)
            .forEach(name -> log.info("FieldName = {}", name));

    System.out.println();

    // java.lang.reflect.Modifiers 클래스에 정의된 16진수 상수값을 OR연산하여 반환하는 값
    Arrays.stream(fields)
            .map(Field::getModifiers)
            .forEach(mod -> log.info("FieldModifiers = {}", mod));

    // constructor name
    Constructor<?>[] constructors = clazz.getDeclaredConstructors();

    System.out.println("============================================================================================================================\n");

    System.out.println("3. 생성자 관련 정보 출력");
    Arrays.stream(constructors)
            .map(Constructor::getName)
            .forEach(name -> log.info("ConstructorName = {}", name));

    Arrays.stream(constructors)
            .map(Constructor::getModifiers)
            .forEach(mod -> log.info("ConstructorModifiers = {}", mod));

    Arrays.stream(constructors)
            .map(Constructor::getParameterTypes)
            .forEach(param -> Arrays.stream(param).forEach(p -> log.info("ParameterType = {}", p)));

    System.out.println("============================================================================================================================\n");

    // method
    System.out.println("4. 메소드 관련 정보 출력");

    Method[] methods = clazz.getDeclaredMethods();
    Arrays.stream(methods)
            .map(Method::getName)
            .forEach(name -> log.info("MethodName = {}", name));

    Arrays.stream(methods)
            .map(Method::getModifiers)
            .forEach(mod -> log.info("MethodModifiers = {}", mod));

    Arrays.stream(methods)
            .map(Method::getParameterTypes)
            .forEach(param -> Arrays.stream(param).forEach(p -> log.info("ParameterType = {}", p)));

}
  • Question 클래스의 모든 필드, 생성자, 메소드에 대한 정보를 출력할 수 있습니다.

  • getFields, getConstructors, getMethods는 접근제어자가 public인 것들만 가져옵니다.

  • getDeclaredXxx로 시작하는 메소드는 접근제어자가 private인 것들도 포함하여 가져옵니다.

  • int getModifiers() 메소드는 접근제어자가 어떤게 붙어있는지 알아내기 위해 16진수로 정의된 상수값들을 OR연산하여 값을 반환합니다.

    image

    • 위 값들은 2진수의 각 자릿수를 나타내기 떄문에, 10진수 값을 다시 2진수로 역변환하여 어떤 접근제어자가 사용되었는지 알아낼 수 있을것으로 예상합니다.



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

public class Junit3Runner {

		@Test
		public void runner() throws Exception {
		    Class clazz = Junit3Test.class;
		    Method[] methods = clazz.getDeclaredMethods();
		    Object instance = clazz.getDeclaredConstructor().newInstance();
		
		    for (Method method : methods) {
		        if (method.getName().startsWith("test")) {
		            method.setAccessible(true); // private 접근제어자를 가진 메소드를 실행할 수 있는 권한을 준다.
		            method.invoke(instance);
		        }
		    }
		}
}

image

  • Class에서 직접 newInstance()를 하는 방식은 Java 9부터 Deprecated 되었기에 Class.getConstructor()를 통해 생성자를 가져오고, 생성자를 이용해 newInstance()를 해야 합니다.
  • 다만 주의할 부분은, 메소드 정보만 있다고 해당 메소드를 실행 시킬 수 있는게 아니라, 먼저 메소드를 실행 시킬 클래스의 인스턴스를 생성한 후, Method.invoke()인자에 해당 인스턴스를 같이 넘겨줘야 실행할 수 있습니다.



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

/**
 * @Testable을 붙이면 IDE에서 테스트 가능한 메소드를 추적해 Run 마크를 달아줌
 * https://junit.org/junit5/docs/5.2.0/api/org/junit/platform/commons/annotation/Testable.html
 */
public class Junit4Runner {
    @Test
    public void run() throws Exception {
        Class clazz = Junit4Test.class;
        Method[] methods = clazz.getDeclaredMethods();
        Object instance = clazz.getDeclaredConstructor().newInstance();

        for (Method method : methods) {
//            if (Objects.nonNull(method.getAnnotation(MyTest.class))) {
            if (method.isAnnotationPresent(MyTest.class)) {
                method.setAccessible(true); // private 접근제어자를 가진 메소드를 실행할 수 있는 권한을 준다.
                method.invoke(instance);
            }
        }
    }
}
  • Method에게 isAnnotationPresent(AnnotationType.class)를 넘겨주면 AnnotationType이라는 애노테이션이 해당 메소드에 붙어있는지 확인하고 boolean값을 반환합니다.
    • 다른 방법으로는 메소드에 있는 애노테이션 값을 직접 가져와서 비교하는 방식도 있는데, 해당 애노테이션이 없다면 null을 반환하므로 주의해야 합니다.
  • 추가로 JUnit은 어떻게 @Test 붙여도 인텔리제이에서 실행 마크를 띄워주는지 궁금했는데, @Testable을 붙이면 IDE에서 테스트 가능한 메소드를 추적해 Run 마크를 달아주는 것을 알 수 있었습니다.



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

@Test
void privateFieldAccess() throws Exception {
    // given
    Class<Student> studentClass = Student.class;
    Student student = studentClass.getConstructor().newInstance();
    log.debug(studentClass.getName());

    // when
    Field name = studentClass.getDeclaredField("name");
    Field age = studentClass.getDeclaredField("age");
    name.setAccessible(true);
    name.set(student, "이름");
    age.setAccessible(true);
    age.setInt(student, 10);

    // then
    assertAll(
            () -> assertThat(student.getName()).isEqualTo("이름"),
            () -> assertThat(student.getAge()).isEqualTo(10)
    );
}
  • Student 클래스의 필드는 private입니다. 따라서 private 필드에 접근할 수 있는 권한을 얻기 위해 Field.setAccessible(true)를 넘겨주어 내가 해당 필드에 접근할 수 있는 권한을 얻습니다.
  • 이것 역시 필드를 세팅하려면 인스턴스를 만들어 주어야 합니다.



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

public class UserTest {

    @Test
    void userTest() throws Exception {
        // given
        Class<User> userClass = User.class;

        // when
        User user = null;
        Constructor<?>[] constructors = userClass.getConstructors();
        for (Constructor<?> constructor : constructors) {
            Class<?>[] parameterTypes = constructor.getParameterTypes();
            if (parameterTypes.length == 2 && parameterTypes[0] == String.class
                    && parameterTypes[1] == Integer.class) {
                user = (User) constructor.newInstance("철수", 10);
            }
        }

        // then
        assertThat(user.getName()).isEqualTo("철수");
        assertThat(user.getAge()).isEqualTo(10);
    }
}
  • 기본 생성자가 아니라, 인수가 있는 생성자이므로 생성자의 타입이 무엇인지 검증하고, 해당 타입에 알맞는 생성자 인수를 넘겨주어 생성할 수 있습니다.



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

public class ElapsedTimeTest {

    private static final Logger log = LoggerFactory.getLogger(ElapsedTimeTest.class);

    static class TestClass {

        public TestClass() {
        }

        @ElapsedTime
        public void test1() {
            for (int i = 0; i < 1000; i++) {
                System.out.print("");
            }
            System.out.println("test1 실행");
        }

        public void test2() {
            System.out.println("test2 실행");
        }
    }

    @Test
    void elapsedTimeCheck() throws Exception {
        Class<TestClass> testClassClass = TestClass.class;
        TestClass testClass = testClassClass.getConstructor().newInstance();

        Method[] methods = testClassClass.getDeclaredMethods();
        for (Method method : methods) {
            if (method.isAnnotationPresent(ElapsedTime.class)) {
                method.setAccessible(true);
                long currentTimeMillis = System.currentTimeMillis();
                method.invoke(testClass);
                long elapsedTimeMillis = System.currentTimeMillis() - currentTimeMillis;

                log.info("{}ms", elapsedTimeMillis);
            }
        }
    }
}



보너스 미션 2 - 바이트 코드 확인하기

package next;

public class Main {

    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}
  • 바이트코드에서 상수 풀 까지 확인하려면 javap를 사용해야 합니다. 따라서 위 Main.java를 컴파일한 Main.class의 위치에서 다음 명령어를 실행하면 됩니다.

  • javap -v Main.class

    Classfile /Users/woowatech20/develop/wootacam/java-practice-wootecam/out/production/classes/next/Main.class
      Last modified 2024. 7. 8.; size 526 bytes
      SHA-256 checksum c7e78c1fd2b1802ca3ca67a9d5025f210ff186e4294defab95242b0e46ef8fbd
      Compiled from "Main.java"
    public class next.Main
      minor version: 0
      major version: 55
      flags: (0x0021) ACC_PUBLIC, ACC_SUPER
      this_class: #5                          // next/Main
      super_class: #6                         // java/lang/Object
      interfaces: 0, fields: 0, methods: 2, attributes: 1
    Constant pool:
       #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
       #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
       #3 = String             #23            // Hello World!
       #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
       #5 = Class              #26            // next/Main
       #6 = Class              #27            // java/lang/Object
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               LocalVariableTable
      #12 = Utf8               this
      #13 = Utf8               Lnext/Main;
      #14 = Utf8               main
      #15 = Utf8               ([Ljava/lang/String;)V
      #16 = Utf8               args
      #17 = Utf8               [Ljava/lang/String;
      #18 = Utf8               SourceFile
      #19 = Utf8               Main.java
      #20 = NameAndType        #7:#8          // "<init>":()V
      #21 = Class              #28            // java/lang/System
      #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
      #23 = Utf8               Hello World!
      #24 = Class              #31            // java/io/PrintStream
      #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
      #26 = Utf8               next/Main
      #27 = Utf8               java/lang/Object
      #28 = Utf8               java/lang/System
      #29 = Utf8               out
      #30 = Utf8               Ljava/io/PrintStream;
      #31 = Utf8               java/io/PrintStream
      #32 = Utf8               println
      #33 = Utf8               (Ljava/lang/String;)V
    {
      public next.Main();
        descriptor: ()V
        flags: (0x0001) ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 3: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lnext/Main;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: (0x0009) ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=1, args_size=1
             0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #3                  // String Hello World!
             5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
          LineNumberTable:
            line 6: 0
            line 7: 8
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       9     0  args   [Ljava/lang/String;
    }

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

개인 활동 페이지

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

그룹 활동 페이지

🎤 미니 세미나

미니 세미나

🤔 기술 블로그 활동

기술 블로그 활동

📚 도서를 추천해주세요

추천 도서 목록

🎸 기타

기타 유용한 학습 링크

Clone this wiki locally