리플렉션
프로그램이 실행 중(런타임)에 자기 자신의 클래스 구조(필드, 메서드, 생성자 등)를 탐색하고 조작할 수 있는 기능입니다.
일반 코드 vs 리플렉션
// 일반 코드 — 컴파일 타임에 타입을 알고 있음
Person p = new Person("Kim");
p.getName();
// 리플렉션 — 런타임에 타입을 몰라도 동적으로 접근
Class<?> clazz = Class.forName("Person");
Object obj = clazz.getDeclaredConstructor(String.class).newInstance("Kim");
Method m = clazz.getMethod("getName");
String name = (String) m.invoke(obj);
핵심 차이는 컴파일 시점에 클래스를 알아야 하느냐, 런타임에 동적으로 다루느냐입니다.
리플렉션으로 할 수 있는 것
| 기능 | API 예시 | 설명 |
|---|---|---|
| 클래스 정보 조회 | Class.forName("Person") | 클래스 이름(문자열)으로 Class 객체 획득 |
| 필드 접근 | clazz.getDeclaredField("name") | private 필드까지 읽기·쓰기 가능 |
| 메서드 호출 | method.invoke(obj, args) | 메서드 이름(문자열)으로 동적 호출 |
| 생성자 호출 | clazz.newInstance() | 클래스 이름만으로 객체 생성 |
| 어노테이션 조회 | field.getAnnotation(...) | 붙어 있는 어노테이션 정보 읽기 |
동작 원리
컴파일 타임
─────────
Person.java → javac → Person.class
│
▼
런타임 (JVM)
─────────
Class Loader가 Person.class를 Method Area에 적재
│
▼
Method Area에 저장된 정보
┌─────────────────────────────┐
│ 클래스명: Person │
│ 필드: name (String, private) │
│ 메서드: getName(), setName() │
│ 생성자: Person(String) │
│ 어노테이션: @Entity │
└─────────────────────────────┘
│
▼
리플렉션 API가 이 메타정보를 런타임에 읽어서 사용
코드 예시
기본 사용법
class Person {
private String name;
public Person(String name) { this.name = name; }
public String getName() { return name; }
}
// 클래스 정보 획득
Class<?> clazz = Class.forName("Person");
// 생성자로 객체 생성
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);
Object person = constructor.newInstance("Kim");
// 메서드 호출
Method getName = clazz.getMethod("getName");
System.out.println(getName.invoke(person)); // "Kim"
// private 필드 접근
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true); // private 접근 허용
nameField.set(person, "Lee"); // 강제로 값 변경
System.out.println(getName.invoke(person)); // "Lee"
어노테이션 기반 활용
@Retention(RetentionPolicy.RUNTIME)
@interface MyColumn {
String value();
}
class User {
@MyColumn("user_name")
private String name;
@MyColumn("user_age")
private int age;
}
// 리플렉션으로 어노테이션 읽기
for (Field field : User.class.getDeclaredFields()) {
MyColumn col = field.getAnnotation(MyColumn.class);
if (col != null) {
System.out.println(field.getName() + " → " + col.value());
}
}
// 출력:
// name → user_name
// age → user_age
실무에서 리플렉션을 쓰는 곳
| 프레임워크 | 사용 방식 |
|---|---|
| Spring DI | @Autowired 붙은 필드를 찾아 자동으로 객체 주입 |
| Spring MVC | @GetMapping 메서드를 찾아 URL과 매핑 |
| JPA/Hibernate | @Entity 클래스의 필드를 읽어 SQL 자동 생성 |
| Jackson/Gson | 객체 필드를 탐색해 JSON ↔ 객체 변환 |
| JUnit | @Test 붙은 메서드를 찾아 자동 실행 |
| Lombok | 컴파일 타임에 어노테이션을 읽어 코드 생성 |
Spring이 리플렉션을 사용하는 과정 예시
@Service
public class UserService {
@Autowired
private UserRepository repo; // 개발자는 그냥 어노테이션만 붙임
}
Spring 컨테이너 내부 (리플렉션 사용)
───────────────────────────────────
1. 클래스패스 스캔 → @Service 붙은 클래스 발견
2. Class.forName("UserService")
3. constructor.newInstance() → 객체 생성
4. getDeclaredFields() → @Autowired 필드 발견
5. field.setAccessible(true)
6. field.set(userService, repoInstance) → 의존성 주입 완료
리플렉션의 단점
| 단점 | 설명 |
|---|---|
| 성능 저하 | 일반 호출 대비 수 배 ~ 수십 배 느림 |
| 타입 안전성 상실 | 컴파일러가 오류를 잡지 못하고 런타임에 터짐 |
| 캡슐화 파괴 | private 접근이 가능해 객체 설계 원칙 위반 |
| 디버깅 어려움 | 문자열 기반 호출이라 IDE 추적이 힘듦 |
| AOT 컴파일 제한 | GraalVM Native Image에서 리플렉션 사용 시 별도 설정 필요 |
성능 비교
// 일반 호출 — ~1ns
person.getName();
// 리플렉션 호출 — ~100ns (수십 배 느림)
method.invoke(person);
AOT에서 문제가 되는 이유
[ JVM 실행 ]
런타임에 Class.forName("Person") 호출
→ Method Area에서 Person 메타정보를 찾아 반환 O
[ AOT (GraalVM Native Image) ]
빌드 시점에 모든 클래스를 결정해야 함
→ Class.forName("Person")은 런타임에 어떤 문자열이 올지 모름
→ 빌드 시점에 Person을 포함할지 판단 불가 X
→ reflect-config.json에 명시적으로 등록해야 함
핵심 정리
리플렉션은 런타임에 클래스의 구조를 탐색하고 동적으로 조작하는 기능입니다. Spring, JPA, Jackson 등 거의 모든 Java 프레임워크의 핵심 동작 원리이지만, 성능 저하·타입 안전성 상실·AOT 호환 문제라는 트레이드오프가 있습니다. 따라서 프레임워크 내부에서는 광범위하게 사용되지만, 애플리케이션 코드에서 직접 사용하는 것은 가급적 피하는 것이 권장됩니다.