Java Reflection은 실행 중인 Java 프로그램이 자신의 구조를 검사하고 수정할 수 있게 해주는 API입니다. 코드가 실행되는 동안 클래스, 인터페이스, 필드, 메서드의 내부 정보에 접근하여 프로그램을 더 동적으로 만들 수 있는 기능입니다.
Reflection은 자바 프로그래밍 언어에서 제공하는 API로, 실행 중인 프로그램의 클래스, 인터페이스, 필드, 메서드에 대한 정보를 조사하고 동적으로 조작할 수 있게 해줍니다. 이렇게 저장된 클래스에 대한 정보가 마치 거울에 투영된 모습과 닮아있어, reflection이라 합니다.
이 기능은 java.lang.reflect 패키지에 포함되어 있습니다.
Java Reflection을 통해 할 수 있는 주요 작업들은 다음과 같습니다.
- 클래스의 모든 멤버(필드, 메서드, 생성자 등) 검색
- 접근 제한자에 관계없이 객체의 필드 값 가져오기 및 수정하기
- 메서드를 동적으로 호출하기
- 런타임에 객체 생성하기
- 애너테이션 정보 조회하기
바로 JVM의 메모리 영역입니다. 애플리케이션을 실행하면 작성한 자바 코드는 컴파일러에 의해 .class 형태의 바이트 코드로 변환되고, 이 정보들은 클래스 로더를 통해 JVM 메모리 영역에 저장됩니다. 그리고 클래스 정보를 통해 객체가 생성된다면 이는 JVM 힙 영역에 저장됩니다.

즉, Reflection은 JVM 메모리 영역에 로드된 클래스의 메타데이터에 접근하여 해당 정보를 가져오고 조작하는 것입니다. 이것이 Reflection이 런타임에 동작할 수 있는 이유이자, 컴파일 타임 검사를 우회할 수 있는 이유이기도 합니다.
// 클래스 정보 가져오기
Class<?> clazz = Class.forName("java.util.ArrayList");
System.out.println("클래스 이름: " + clazz.getName());
System.out.println("단순 클래스 이름: " + clazz.getSimpleName());
System.out.println("패키지 이름: " + clazz.getPackageName());
// 모든 메서드 확인하기
Method[] methods = clazz.getDeclaredMethods();
System.out.println("클래스의 메서드 목록:");
for (Method method : methods) {
System.out.println("\t" + method.getName() + ": " + method.getReturnType());
}
// 모든 필드 확인하기
Field[] fields = clazz.getDeclaredFields();
System.out.println("클래스의 필드 목록:");
for (Field field : fields) {
System.out.println("\t" + field.getName() + ": " + field.getType());
}
// 리플렉션으로 객체 생성
Class<?> stringClass = Class.forName("java.lang.String");
Constructor<?> constructor = stringClass.getConstructor(String.class);
Object stringObject = constructor.newInstance("Hello Reflection");
// 메서드 호출
Method toUpperCaseMethod = stringClass.getMethod("toUpperCase");
Object result = toUpperCaseMethod.invoke(stringObject);
System.out.println(result); // "HELLO REFLECTION" 출력
단순히 조회 뿐 아니라 멤버필드의 값도 수정할 수 있습니다.
Setter 메서드가 없어도, 접근제어자가 private라도 이를 무시하고 값을 바꿔버릴 수 있는 것 입니다.
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
// 메인 코드
Person person = new Person("John");
Field nameField = Person.class.getDeclaredField("name");
nameField.setAccessible(true); // private 접근 제한자 우회
String name = (String) nameField.get(person);
System.out.println("이름: " + name); // "이름: John" 출력
// 필드 값 변경
nameField.set(person, "Jane");
System.out.println("변경된 이름: " + nameField.get(person)); // "변경된 이름: Jane" 출력
Spring, Hibernate, JUnit과 같은 프레임워크들은 Reflection을 광범위하게 사용합니다. 예를 들어, Spring은 애너테이션이 달린 클래스를 스캔하고 빈으로 등록하기 위해 Reflection을 사용합니다.
JUnit과 같은 테스트 프레임워크는 @Test 애너테이션이 달린 메서드를 찾고 실행하기 위해 Reflection을 사용합니다.
런타임에 플러그인을 로드하고 통합해야 하는 시스템에서는 Reflection이 필수적입니다. 컴파일 시점에 존재하지 않았던 클래스를 동적으로 로드하고 사용할 수 있습니다.
객체를 파일이나 네트워크로 전송하기 위해 JSON, XML 등으로 변환할 때, Reflection은 객체의 모든 필드를 쉽게 접근할 수 있게 해줍니다.
Hibernate와 같은 ORM 도구는 Java 객체와 데이터베이스 테이블 간의 매핑을 위해 Reflection을 사용합니다.
컴파일 시 알려지지 않은 클래스, 메서드, 필드에 접근할 수 있어 매우 유연한 코드를 작성할 수 있습니다.
Reflection은 프레임워크 개발자들이 유연하면서도 강력한 도구를 만들 수 있게 해줍니다. Spring, Hibernate, JUnit 등이 대표적입니다.
프라이빗 필드와 메서드에 접근할 수 있어 테스트가 더 쉬워집니다.
경우에 따라, Reflection을 사용하면 더 적은 코드로 더 많은 기능을 구현할 수 있어 유지보수가 쉬워질 수 있습니다.
Reflection 호출은 일반 메서드나 필드 접근보다 훨씬 느립니다. JVM 최적화를 우회하며, 추가적인 오버헤드가 발생합니다.
// 일반적인 방법
long start = System.nanoTime();
for (int i = 0; i < 1000000; i++) {
String str = "hello";
str = str.toUpperCase();
}
System.out.println("General: " + (System.nanoTime() - start) + "ns");

// Reflection 방법
start = System.nanoTime();
Method toUpperCase = String.class.getMethod("toUpperCase");
for (int i = 0; i < 1000000; i++) {
String str = "hello";
str = (String) toUpperCase.invoke(str);
}
System.out.println("Reflection: " + (System.nanoTime() - start) + "ns");

위 코드를 실행하면 Reflection이 일반 방법보다 3배 이상 느린 것을 확인할 수 있습니다.
Reflection은 컴파일 타임 타입 검사를 우회하므로, 런타임 에러가 발생할 가능성이 높아집니다.
// 컴파일 시 발견 불가능한 오류
try {
Class<?> clazz = Class.forName("com.example.NonExistentClass");
Method method = clazz.getMethod("nonExistentMethod");
} catch (Exception e) {
System.out.println("런타임 오류 발생: " + e.getMessage());
}
Reflection은 접근 제한자(private, protected)를 우회할 수 있어 캡슐화를 깨트릴 수 있습니다. 이는 의도하지 않은 부작용을 일으킬 수 있으며, 보안 취약점이 될 수 있습니다.
Reflection을 사용한 코드는 일반 코드보다 더 복잡하고 이해하기 어려울 수 있습니다. 또한, 디버깅도 어려워집니다.
Reflection으로 접근하는 클래스의 내부 구조가 변경되면, 런타임 오류가 발생할 수 있습니다.
Reflection은 강력한 도구이지만 모든 상황에 적합하지는 않습니다.
다음과 같은 상황에서 주로 사용하는 것이 좋습니다.
반면, 다음과 같은 상황에서는 Reflection 사용을 피하는 것이 좋습니다.
Java Reflection은 런타임에 클래스, 메서드, 필드 등의 정보를 동적으로 조회하고 조작할 수 있도록 해주는 기능입니다. 이를 활용하면 객체의 타입을 미리 알지 않고도 메서드를 호출하거나 필드 값을 수정할 수 있습니다.
유연성 증가
프레임워크 및 라이브러리 개발에 필수적
컴파일 의존성 제거
성능 저하
타입 안전성 부족
NoSuchMethodException 등의 예외가 발생할 가능성 존재 보안 문제
디버깅 어려움
애너테이션 프로세싱 (Annotation Processing)
java.lang.reflect.Proxy를 활용한 인터페이스 기반 동적 프록시 생성https://tlatmsrud.tistory.com/112
파랑, 아키의 리플렉션 - https://www.youtube.com/watch?v=67YdHbPZJn4