-
[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); } }
이제 JUnit5와 AssertJ를 사용하여 테스트해보자.
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 객체로 관리하며, 이를 통해 클래스 구조에 접근할 수 있다.
리플렉션 동작 과정
- 클래스 로딩
- 클래스 로더가 컴파일된 .class 파일을 JVM으로 가져온다.
- JVM은 Class 객체를 생성하고 클래스 메타데이터를 저장한다.
- Class 객체 -> Heap Area
- Class 객체는 메서드 영역에 저장된 클래스 메타데이터를 참조
- 클래스 메타데이터(클래스 이름, 필드, 메서드, 상수 등) -> Method Area
- Class 객체 -> Heap Area
- 메타데이터 접근
- 리플렉션은 런타임에 Class 객체를 통해 클래스 메타데이터에 접근한다.
- Class 객체를 기반으로 메서드, 필드, 생성자를 가져온다.
- 동적 조작
- 리플렉션 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