리플렉션

프로그램이 실행 중(런타임)에 자기 자신의 클래스 구조(필드, 메서드, 생성자 등)를 탐색하고 조작할 수 있는 기능입니다.

일반 코드 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 호환 문제라는 트레이드오프가 있습니다. 따라서 프레임워크 내부에서는 광범위하게 사용되지만, 애플리케이션 코드에서 직접 사용하는 것은 가급적 피하는 것이 권장됩니다.

CSLOG에서 보기