ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] 리플렉션(Reflection)에 대해서
    Java 2025. 1. 17. 23:04

    자바의 리플렉션이란 런타임에 클래스, 생성자, 필드, 메서드 등에 동적으로 접근하여 조작할 수 있게 해주는 기능이다. 긴말 필요 없이 예제 코드로 알아보자.

     

    간단한 계산기 클래스를 만들었다.

    public class Calculator {
    
        private int add(int a, int b) {
            return a + b;
        }
    
        public static int max(int a, int b) {
            return Math.max(a, b);
        }
    }

     

     

    이제 JUnit5AssertJ를 사용하여 테스트해보자.

    class ReflectionTest {
    
        @Test
        void testCalculator_1() throws Exception {
            Class<?> calculatorClass = Class.forName("reflection.Calculator");
    
            Constructor<?> constructor = calculatorClass.getDeclaredConstructor();
            
            // newInstance()는 항상 Object 타입을 반환
            Calculator calculator = (Calculator) constructor.newInstance();
    
            // 메서드명, 매개변수 타입을 통해 메서드 조회
            Method addMethod = calculatorClass.getDeclaredMethod("add", int.class, int.class);
            addMethod.setAccessible(true); // private 생성자 접근 허용
    
            int a = 5, b = 3;
            int answer = a + b;
            // 인스턴스 메서드 호출 시 인스턴스를 제공해줘야 함!
            int result = (int) addMethod.invoke(calculator, a, b);
    
            assertThat(result).isEqualTo(answer);
        }
    
        // static 메서드 테스트
        @Test
        void testCalculator_2() throws Exception {
            int a = 10, b = 20;
            int answer = Math.max(a, b);
    
            Class<?> calculatorClass = Class.forName("reflection.Calculator");
            Method maxMethod = calculatorClass.getDeclaredMethod("max", int.class, int.class);
            
            // static 메서드는 인스턴스 생성 없이 null을 넣고 사용 가능
            int result = (int) maxMethod.invoke(null, a, b);
    
            assertThat(result).isEqualTo(answer);
        }
     }

    테스트 성공!

     


    다음 예제로 Student 클래스를 만들어 보았다.

    @AllArgsConstructor
    @EqualsAndHashCode(of = {"name", "score"})
    public class Student {
    
        private String name;
        private int score;
    }

     

     

    다시 테스트 코드 작성!

    @Test
    void student() throws Exception {
        String name = "StudentA";
        int score = 90;
    
        Class<?> studentClass = Class.forName("reflection.Student");
        
        // 생성자 매개변수 타입 제공
        Constructor<?> constructor = studentClass.getDeclaredConstructor(String.class, int.class);
        Student studentA = (Student) constructor.newInstance(name, score);
    
        assertThat(studentA).isEqualTo(new Student(name, score));
    }

    테스트 성공!


     

    이제 Student 클래스를 약간 수정하고, 리플렉션 관련 Exception에 대해 알아보자.

    @AllArgsConstructor
    public class Student {
        private final String name;
        private final int score;
    
        private String getDetails() {
            return "Name: " + name + ", Score: " + score;
        }
    
        public void throwException() throws RuntimeException {
            throw new RuntimeException("This is a test exception");
        }
    }

     

    1. ClassNotFoundException

    • 지정한 클래스 이름이 잘못되었거나 클래스가 클래스 패스에 없는 경우에 발생한다.
    @Test
    void testClassNotFoundException() {
        assertThatThrownBy(() -> Class.forName("reflection.students")) // exception!
                .isExactlyInstanceOf(ClassNotFoundException.class);
    }

     

     

    2. NoSuchMethodException

    • 호출하려는 메서드 또는 생성자가 존재하지 않을 때 발생한다.
    @Test
    void testNoSuchMethodException() throws Exception {
        Class<?> studentClass = Class.forName("reflection.Student");
        
        // 메서드 존재 X
        assertThatThrownBy(() -> studentClass.getDeclaredMethod("nonExistentMethod"))
                .isExactlyInstanceOf(NoSuchMethodException.class);
    
        // 기본 생성자 존재 X
        assertThatThrownBy(() -> studentClass.getDeclaredConstructor())
                .isExactlyInstanceOf(NoSuchMethodException.class);
    }

     

     

    3. IllegalAccessException

    • 접근 제어가 허용되지 않은 메서드, 필드 등에 접근할 때 발생한다.
    @Test
    void testIllegalAccessException() throws Exception {
        Class<?> studentClass = Class.forName("reflection.Student");
        Student student = (Student) studentClass.getDeclaredConstructor(String.class, int.class).newInstance("name", 90);
        
        // private method
        Method method = studentClass.getDeclaredMethod("getDetails");
    
        assertThatThrownBy(() -> method.invoke(student))
                .isExactlyInstanceOf(IllegalAccessException.class);
    }

     

     

    4. IllegelArgumentException

    • 메서드나 생성자에 매개변수를 잘못 전달했을 때 발생한다.
    @Test
    void testIllegalArgumentException() throws Exception {
        Class<?> studentClass = Class.forName("reflection.Student");
        Constructor<?> constructor = studentClass.getDeclaredConstructor(String.class, int.class);
        
        assertThatThrownBy(() -> constructor.newInstance())
                .isExactlyInstanceOf(IllegalArgumentException.class);
    }

     

     

    5. InvocationTargetException

    • 리플렉션을 통해 호출된 메서드에서 예외가 발생했을 때 래핑되어 던져지는 예외이다.
    @Test
    void testInvocationTargetException() throws Exception {
        Class<?> studentClass = Class.forName("reflection.Student");
        Constructor<?> constructor = studentClass.getDeclaredConstructor(String.class, int.class);
        Student student = (Student) constructor.newInstance("StudentA", 90);
    
        Method method = studentClass.getDeclaredMethod("throwException");
    
        assertThatThrownBy(() -> method.invoke(student))
                .isInstanceOf(InvocationTargetException.class) // 래핑된 예외
                .hasCauseInstanceOf(RuntimeException.class) // 실제 예외
                .hasRootCauseMessage("This is a test exception");
    }

     

     

    테스트 성공!


    리플렉션의 원리

    리플렉션은 Class 객체와 JVM을 통해 이루어진다. JVM은 모든 클래스에 대한 데이터를 Class 객체로 관리하며, 이를 통해 클래스 구조에 접근할 수 있다.

     

    리플렉션 동작 과정

    1. 클래스 로딩
      • 클래스 로더가 컴파일된 .class 파일을 JVM으로 가져온다.
      • JVM은 Class 객체를 생성하고 클래스 메타데이터를 저장한다.
        • Class 객체 -> Heap Area
          • Class 객체는 메서드 영역에 저장된 클래스 메타데이터를 참조
        • 클래스 메타데이터(클래스 이름, 필드, 메서드, 상수 등) -> Method Area
    2. 메타데이터 접근
      • 리플렉션은 런타임에 Class 객체를 통해 클래스 메타데이터에 접근한다.
      • Class 객체를 기반으로 메서드, 필드, 생성자를 가져온다.
    3. 동적 조작
      • 리플렉션 API를 통해 동적으로 생성자, 메서드를 호출하고 필드 값을 읽거나 변경한다.

     

    리플렉션의 한계

    • 코드가 복잡해져 가독성이 떨어질 수 있다.
    • 런타임 오류 발생 가능성이 다소 높다.
    • 런타임에 탐색이 이루어지기 때문에 성능 저하가 발생할 수 있다.
    • 소거된 제네릭 타입에 대한 정보를 얻을 수 없음

    'Java' 카테고리의 다른 글

    [Java] volatile 예약어  (1) 2025.02.18
    [Java] record 예약어  (1) 2025.02.13
    [Java] I/O 정리  (1) 2024.11.09
    [Java] Charset과 문자 인코딩  (0) 2024.11.08
    [Java] 다형성 (Polymorphism)  (0) 2024.11.07