[Java] Reflection: Class, Method, Field, MethodHandle
정의
Reflection 은 런타임에 클래스 메타데이터 (필드, 메서드, 어노테이션, 생성자) 를 조회하고, 컴파일 타임에 알 수 없는 객체를 생성/조작할 수 있게 해 주는 Java API. java.lang.reflect.* 패키지.
Spring 의 DI, Jackson 의 직렬화, JPA 의 entity 매핑, JUnit 의 테스트 메서드 호출 모두 reflection 위에서 동작. 단, 성능 비용 + 캡슐화 위반 + JVM 최적화 방해 같은 단점이 있어 일반 코드에서는 피하고 framework 가 대신 써 준다.
Class 객체 얻기
Class<String> c1 = String.class; // 컴파일 타임 알 때
Class<?> c2 = "hello".getClass(); // 인스턴스에서
Class<?> c3 = Class.forName("com.example.User"); // 런타임 이름으로
Class<?> c4 = ClassLoader.getSystemClassLoader().loadClass("com.example.User");
Class.forName 은 클래스를 로드하고 static initializer 실행. JDBC driver 등록 같은 부수효과가 있는 코드에 사용 (Class.forName("org.postgresql.Driver")).
메타데이터 조회
import java.lang.reflect.*;
public class MetaDemo {
static class User {
private String name;
public int age;
public User() {}
public String greet(String prefix) { return prefix + " " + name; }
private void secret() {}
}
public static void main(String[] args) {
Class<?> c = User.class;
System.out.println("declared fields:");
for (Field f : c.getDeclaredFields()) {
System.out.printf(" %s %s%n", Modifier.toString(f.getModifiers()), f.getName());
}
System.out.println("declared methods:");
for (Method m : c.getDeclaredMethods()) {
System.out.printf(" %s %s(%d)%n",
Modifier.toString(m.getModifiers()), m.getName(), m.getParameterCount());
}
}
}declared fields:
private name
public age
declared methods:
public greet(1)
private secret(0)getDeclaredX() 는 private 포함 직접 선언만. getX() 는 public + 상속받은 것. 이름이 헷갈리니 주의.
동적 호출
Class<?> c = Class.forName("com.example.User");
Object instance = c.getDeclaredConstructor().newInstance(); // no-args ctor
Method greet = c.getMethod("greet", String.class);
String result = (String) greet.invoke(instance, "Hello,"); // greet("Hello,")
Field name = c.getDeclaredField("name");
name.setAccessible(true); // private 우회
name.set(instance, "Alice");
setAccessible(true) 는 캡슐화 우회. JDK 9 모듈 시스템 도입 후 --add-opens JVM 옵션이 없으면 외부 모듈에는 적용 안 됨.
어노테이션 처리
import java.lang.annotation.*;
import java.lang.reflect.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Cached { int ttlSeconds() default 60; }
class UserService {
@Cached(ttlSeconds = 300)
public String findById(Long id) { return "user:" + id; }
public void update(Long id) {}
}
public class AnnoDemo {
public static void main(String[] args) {
for (Method m : UserService.class.getDeclaredMethods()) {
Cached a = m.getAnnotation(Cached.class);
if (a != null) {
System.out.printf("%s cached ttl=%d%n", m.getName(), a.ttlSeconds());
}
}
}
}findById cached ttl=300@Retention(RUNTIME) 가 필수. SOURCE / CLASS 로 선언된 어노테이션은 reflection 으로 조회 불가.
동적 프록시
interface 기반 프록시 생성 (AOP 의 기본):
interface Greeter {
String greet(String name);
}
Greeter proxy = (Greeter) Proxy.newProxyInstance(
Greeter.class.getClassLoader(),
new Class<?>[] { Greeter.class },
(p, method, args) -> {
System.out.println("→ " + method.getName());
return "Hello, " + args[0];
}
);
System.out.println(proxy.greet("World"));
// → greet
// Hello, World
interface 가 아닌 클래스를 프록시하려면 CGLIB / ByteBuddy 같은 바이트코드 라이브러리 필요. Spring 의 @Configuration proxy 가 그 예.
MethodHandle (JDK 7+, 현대적 대안)
java.lang.invoke.MethodHandle 은 reflection 보다 빠르고 JIT 친화적:
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle greet = lookup.findVirtual(
User.class,
"greet",
MethodType.methodType(String.class, String.class)
);
String result = (String) greet.invoke(user, "Hi,");
특징:
- JIT 최적화 가능: 반복 호출이 reflection 보다 훨씬 빠름 (거의 native call 수준)
- MethodType 으로 정확한 시그니처 명시
- VarHandle: 필드 접근의 MethodHandle 대응 (atomic, volatile semantics)
- Lookup 의 권한: 호출한 클래스의 접근 권한이 그대로 적용 (캡슐화 보존)
라이브러리 (Jackson 신버전, ByteBuddy) 가 reflection 대신 MethodHandle 로 전환 중.
성능 비용
직접 호출: 1x (baseline)
MethodHandle: 2-3x
Reflection: 30-100x (cache 안 했을 때)
Reflection (cached): 5-10x
framework 들이 Method 객체를 캐싱해 두는 이유. 매번 getMethod() 호출은 절대 금물.
JDK 17 이후 JIT 가 많이 개선돼 cached reflection 의 차이는 줄었지만 여전히 hot path 에서는 피하는 게 좋다.
함정과 베스트 프랙티스
- JDK 9+ 모듈 시스템:
setAccessible(true)가 외부 모듈에 막힘.--add-opens java.base/java.lang=ALL-UNNAMED같은 JVM 옵션 필요 getName()vsgetCanonicalName()vsgetSimpleName(): inner class / array 에서 결과 다름isAssignableFrom방향 주의:Animal.class.isAssignableFrom(Dog.class)는 true (반대로 안 됨)- Generic type erasure: 런타임에
List<String>의String정보 없음.ParameterizedType으로 super class / field declaration 에서만 추출 가능 (Spring 의ResolvableType활용) Class.forName의 static 초기화 부수효과 주의- GraalVM Native Image: reflection 사용 클래스는 미리
reflect-config.json에 등록 필요 - JIT 친화적이려면 MethodHandle 사용
관련 위키
이 글의 용어 (5개)
- [Java] JavaBean 프로퍼티 규약java
- 정의 JavaBean 은 다음 세 가지 규약을 만족하는 클래스: 1. public no-args constructor (인자 없는 public 생성자) 2. 프로퍼티 접근자: /…
- [Spring] AOP: @Aspect, Pointcut, Advicespring
- 정의 AOP (Aspect-Oriented Programming)는 cross-cutting concerns(로깅, 트랜잭션, 보안 등)를 비즈니스 코드에서 분리하는 기법. Sp…
- [Spring] BeanPostProcessor와 BeanFactoryPostProcessorspring
- 정의 Spring 컨테이너의 2가지 확장점: - BeanFactoryPostProcessor (BFPP): Bean 인스턴스가 만들어지기 전, (메타데이터) 을 수정. 클래스명,…
- [Spring] IoC와 DI: Bean, ApplicationContext, @Autowiredspring
- 정의 IoC (Inversion of Control)는 객체 생성/생명주기 제어를 프레임워크가 가져가는 원칙. DI (Dependency Injection)는 그 구현 방법. S…
- [Spring] Reflection 유틸: ReflectionUtils, AnnotationUtils, ResolvableTypespring
- 정의 Spring 은 reflection 을 헤비하게 쓴다. DI 처리, AOP 프록시, 해결, Jackson 통합, JPA entity 매핑 모두 reflection 위에서 동…
💬 댓글