Skip to content

윤중진, 김수현 리플렉션

김수현 edited this page Jul 8, 2024 · 2 revisions

Java Reflection을 사용해 본 경험

  • Interceptor에서 메소드에 적용된 Annotation 가져올때
  • 필드에 붙은 Validation Annotation 가져올때 : ex) Password에 대해서 여러가지 유효성 검사를 하고 싶을때 커스텀 어노테이션 @Password를 설정하고 Validator 구현시 필드에 붙은 annotation 정보를 가져온다.

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

    @Test
    @DisplayName("테스트1: 리플렉션을 이용해서 클래스와 메소드의 정보를 정확하게 출력해야 한다.")
    public void showClass() {
        Class<Question> clazz = Question.class;
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            logger.debug("field: {}", field);
        }
        Constructor<?>[] constructors = clazz.getDeclaredConstructors();
        for (Constructor<?> constructor : constructors) {
            logger.debug("constructor: {}", constructor);
        }
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            logger.debug("method: {}", method);
        }
    }
  • 모든 필드, 생성자, 메소드에 대한 정보를 출력한다 : getFields ()아니라 declaredFields 사용()

  • 이슈 : gradle 로 실행하면 하면 no test found 에러발생하는데 intellij로 실행하면 된다.

    • 해결책 1 : build.gradle에서 테스트 플랫폼 명시적으로 설정. Junit5부터 gradle test시 테스트 플랫폼 명시적으로 지정해주어야함.
    test{
    	userJunitPlatform()
    }
    • 해결책 2: intellij 설정에서 gradle > run test using intellij IDEA 설정. intellij에서만 적용되므로 권장되지 않음.

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

	@Test
	public void runner() throws Exception {
		Class<Junit3Test> clazz = Junit3Test.class;
		Method[] declaredMethods = clazz.getDeclaredMethods();
		Junit3Test junit3Test = clazz.getDeclaredConstructor().newInstance();
		for (Method method : declaredMethods) {
			if (method.getName().startsWith("test")) {
				method.invoke(junit3Test);
			}
		}
	}
  • public Object invoke(Object obj, Object... args)
    • 객체에 대해서 메소드를 호출해야하므로 obj에 생성한 객체를 넘겨주어야한다.
  • clazz.newInstance()는 deprecate된 이유
    • 생성자에서 발생한 예외를 직접 전달하지 않고 래핑해서 전달해서 예외의 원인을 파악하기 어렵다.
    • public 기본 생성자가 무조건 필요하다
  • clazz.getDeclaredConstructor().getInstance(); 가 권장됨
    • 생성자에서 발생한 예외를 직접 전달한다.
    • private 생성자에도 접근이 가능하다.
    • 매개변수를 받을 수 있는 생성자를 지정할 수 있다. 다양한 형태의 생성자 호출이 가능하다.
  • 참고 : private 생성자에 대해서 newInstance()를 호출하려면 setAccessible(true)를 통해 접근 가능하도록 설정해주어야한다. 실제로 메소드 영역에 있는 클래스 정보가 변경되는 것은 아님. 리플렉션 객체에 대한 접근 제어 검사를 무시하도록 설정.

요구사항3. Test 어노테이션 메소드 실행

	@Test
	public void run() throws Exception {
		Class<Junit4Test> clazz = Junit4Test.class;
		Method[] declaredMethods = clazz.getDeclaredMethods();
		Junit4Test junit4Test = clazz.getDeclaredConstructor().newInstance();
		for (Method method : declaredMethods) {
			MyTest annotation = method.getAnnotation(MyTest.class);
			if (annotation != null) {
				method.invoke(junit4Test);
			}
		}
	}
  • getAnnotation() vs getDeclaredAnnotation() 같은가
    • getAnnotation() : 상속된 어노테이션도 포함
    • getDeclaredAnnotation() : 상속된 어노테이션 포함 X

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

  • private 필드 값 수정시 Field.setAccessible(true) 필요.
    @Test
    public void privateFieldAccess() throws NoSuchFieldException, IllegalAccessException {
        Class<Student> clazz = Student.class;
        Field nameField = clazz.getDeclaredField("name");
        nameField.setAccessible(true);
        Field ageField = clazz.getDeclaredField("age");
        ageField.setAccessible(true);

        Student student = new Student();
        nameField.set(student, "윤중진");
        ageField.set(student, 27);

        logger.debug("{}", student);
    }

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

  • 생성자의 파라미터 타입을 비교.
  • 해당하는 파라미터 타입을 가진 생성자 없으면 NoSuchMethodException 터지기 때문에 Arrays.equals()로 파라미터 타입 비교한 후에 생성자 호출하는 것이 안전
    @Test
    public void createUserInstance() throws Exception {
        Class<User> clazz = User.class;
        Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
        Class<?>[] expectedParameterTypes = {String.class, Integer.class};
        for (Constructor<?> constructor : declaredConstructors) {
            Class<?>[] parameterTypes = constructor.getParameterTypes();
            if (Arrays.equals(parameterTypes, expectedParameterTypes)) {
                User user = clazz.getDeclaredConstructor(expectedParameterTypes).newInstance("윤중진", 27);
                logger.debug("{}", user);
            };
        }
    }

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

  • ReflectionTest 클래스에 대해서 모든 메소드 정보를 가져와서 @ElapsedTime 어노테이션이 있으면 System.nanoTime() 사용해서 경과한 시간 출력.
package next.reflection;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ElapsedTime {
}
    @Test
    public void checkElapsedTime() throws Exception {
        Class<ReflectionTest> clazz = ReflectionTest.class;
        Method[] declaredMethods = clazz.getDeclaredMethods();
        ReflectionTest reflectionTest = clazz.getConstructor().newInstance();
        for (Method method : declaredMethods) {
            if (method.getAnnotation(ElapsedTime.class) != null) {
                long startTime = System.nanoTime();
                method.invoke(reflectionTest);
                long endTime = System.nanoTime();
                logger.debug("{}: {}ns", method.getName(), endTime - startTime);
            }
        }
    }

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

바이트 코드 확인방법

  1. Java 소스 파일 컴파일
javac src/test/java/next/reflection/HelloWorldTest.java
1. 
javac  -c src/test/java/next/reflection/HelloWorldTest
javap -c src/test/java/next/reflection/HelloWorldTest
// 옵션 -c : 바이트코드를 디스어셈블하여 출력하는 옵션
  1. 클래스 파일을 디스어셈블
javap -c src/test/java/next/reflection/HelloWorldTest
// -c :  바이트코드를 디스어셈블하여 출력하는 옵션 
// -v : 클래스파일의 상세한 정보를 디스어셈블하여 출력하는 옵션

javap -c 결과

public class next.reflection.HelloWorldTest { // 클래스 정의
  public next.reflection.HelloWorldTest(); // 생성자 메소드
    Code: 
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  void helloWorldTest();
    Code:
       0: ldc           #7                  // String Hello World
       2: astore_1
       3: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
       6: aload_1
       7: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      10: return
}
  • 클래스와 생성자 분석

  • 0: aload_0: 로컬 변수 테이블의 0번째 인덱스에 있는 ‘this’를 java stack 영역에 로드한다.

  • 1: invokespecial #1: 슈퍼클래스(Object)의 생성자 (’’)를 호출한다.

  • 4: return: 메서드 종료

  • 메서드 분석

  • 0: ldc #7: constant pool에 7번 인덱스에 있는 “Hello World” 문자열을 스택에 로드한다.

  • 2: astore_1: 스택의 최상위 값(”Hello World”)을 로컬 변수 테이블의 1번 인덱스에 저장한다.

  • 3: getstatic #9: java/lange/System 클래스의 out 필드를 스택에 로드한다. 이 변수는 static임

  • 6: aload_1: 로컬 변수 테이블의 1번 인덱스에 있는 값을 스택에 로드한다.

  • 7: invokevirtual #15: java/io/PrintStream 클래스의 println 메소드를 호출한다.

javap -v 결과

Classfile /Users/woowatech04/Desktop/IdeaProjects/java-practice/src/test/java/next/reflection/HelloWorldTest.class
  Last modified 2024. 7. 8.; size 440 bytes
  SHA-256 checksum f619c80aff49e5ddcce5f544c4b042e8261fe3d22325688259c41247c17b5c99
  Compiled from "HelloWorldTest.java"
public class next.reflection.HelloWorldTest
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #21                         // next/reflection/HelloWorldTest
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = String             #8             // Hello World
   #8 = Utf8               Hello World
   #9 = Fieldref           #10.#11        // java/lang/System.out:Ljava/io/PrintStream;
  #10 = Class              #12            // java/lang/System
  #11 = NameAndType        #13:#14        // out:Ljava/io/PrintStream;
  #12 = Utf8               java/lang/System
  #13 = Utf8               out
  #14 = Utf8               Ljava/io/PrintStream;
  #15 = Methodref          #16.#17        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #16 = Class              #18            // java/io/PrintStream
  #17 = NameAndType        #19:#20        // println:(Ljava/lang/String;)V
  #18 = Utf8               java/io/PrintStream
  #19 = Utf8               println
  #20 = Utf8               (Ljava/lang/String;)V
  #21 = Class              #22            // next/reflection/HelloWorldTest
  #22 = Utf8               next/reflection/HelloWorldTest
  #23 = Utf8               Code
  #24 = Utf8               LineNumberTable
  #25 = Utf8               helloWorldTest
  #26 = Utf8               SourceFile
  #27 = Utf8               HelloWorldTest.java
{
  public next.reflection.HelloWorldTest();
    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

  void helloWorldTest();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=2, locals=2, args_size=1
         0: ldc           #7                  // String Hello World
         2: astore_1
         3: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
         6: aload_1
         7: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        10: return
      LineNumberTable:
        line 7: 0
        line 8: 3
        line 9: 10
}

  • javap -v 명령어는 java constnat pool을 포함한 바이트 코드 상세 정보를 보여준다.
  • SHA-256 체크섬: 해당 바이트코드가 여러 운영체제를 타고 전송되는 동안 데이터가 깨지진 않았는지 무결성을 검사함
  • 상수 풀(Constant Pool)
    • #1: java/lang/Object의 생성자 메서드 참조
    • #7: 문자열 상수 “Hello World”
    • #9: java/lang/System.out 필드 참조
    • #15: java/io/PrintStream.println 메서드 참조
  • byte code
    • stack: 해당 메서드에서 사용하는 최대 스택 깊이다.
      • 그래서 stack = 1 메서드 실행 중에 동시에 최대 1개의 항목만 스택에 있을 수 있다.
    • locals: 해당 메서드에서 사용하는 로컬 변수의 개수를 의미한다.
    • args_size: 해당 메서드의 매개변수 크기를 의미한다.

참고1. reflection 사용시 주의사항

  1. 성능 저하: Reflection을 사용하면 일반적인 메소드 호출이나 필드 접근보다 성능이 떨어진다. 런타임에 타입 검사를 수행하고, 접근 제어를 우회하는 등의 작업이 추가로 필요하기 때문이다. 따라서, 성능이 중요한 코드 경로에서는 Reflection을 피하는 것이 좋다.
  2. 캡슐화 위반: Reflection을 사용하면 클래스의 private 필드나 메소드에 접근할 수 있다. 이는 클래스 설계자의 의도를 무시하고, 내부 구현을 직접 조작하게 된다. 캡슐화를 깨뜨리면 유지보수와 디버깅이 어려워지고, 클래스의 변경에 대한 의존성이 높아진다.
  3. 타입 안전성 부족: Reflection을 사용하면 컴파일 타임에 타입 검사가 이루어지지 않는다. 잘못된 타입을 사용하거나 메소드, 필드를 잘못 참조하면 런타임에 예외가 발생한다. 예를 들어, 메소드 이름을 잘못 입력하거나 잘못된 매개변수 타입을 사용할 경우, NoSuchMethodException이나 IllegalAccessException이 발생할 수 있다.
  4. 보안 문제: Reflection을 통해 접근 권한이 없는 필드나 메소드에 접근할 수 있습니다. 이는 보안 취약점을 초래할 수 있다. 예를 들어, 악성 코드가 Reflection을 사용하여 private 필드를 수정하거나 민감한 정보를 노출할 수 있다.

참고2. setAccessible()을 호출하면 클래스 정보가 변경되나

  • setAccessible(true)를 호출하면, Java Reflection API를 통해 접근 제어 검사를 우회할 수 있다.
    • 리플렉션 객체(예: Field, Method, Constructor)에 대한 접근 제어 검사를 무시하도록 설정한다.
    • AccessibleObject 클래스(리플렉션 객체의 슈퍼 클래스)의 메서드이다.
  • 그러나 이로 인해 실제로 메소드 영역에 있는 클래스 정보가 변경되는 것은 아니다. 대신, 접근 제어 검사를 수행하는 JVM의 동작이 변경된다.
    • 이는 해당 객체에 접근할 때 JVM이 수행하는 접근 제어 검사를 무시하도록 설정하는 것이지, 실제 접근 제어자가 변경되는 것은 아니다
    • 이 변경은 해당 리플렉션 객체에만 적용되며, 클래스 전체에 적용되는 것은 아닙니다.

참고3. Reflection API의 반환타입은 런타임에 결정된다

  • User.class.getDeclaredConstructor()이 반환타입이 Constuctor가 안되는 이유 : java 제네릭의 타입 소거는 컴파일 타임에 유효한데, 리플렉션 API는 런타임에 동작한다. 따라서 리플렉션 API는 컴파일타임에 반환 타입을 알 수 없다.
  • User클래스에서 this.getClass()시 반환타입이 User.class가 아니라 Class<? extends User>가 되는 이유?
    • this.getClass()는 런타임 시 실제 객체의 타입을 반환하며, 이는 클래스 상속 계층 구조를 반영한다.

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

개인 활동 페이지

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

그룹 활동 페이지

🎤 미니 세미나

미니 세미나

🤔 기술 블로그 활동

기술 블로그 활동

📚 도서를 추천해주세요

추천 도서 목록

🎸 기타

기타 유용한 학습 링크

Clone this wiki locally