Spring

리플렉션 2트

99duuk 2025. 4. 28. 14:43

리플렉션(Reflection) 요약 정리

리플렉션은
런타임 시점에
클래스, 메서드, 필드, 생성자 등의 메타데이터를 조회하거나 조작할 수 있는 기능이다.


  • 컴파일 시점에 뭐 하는 게 아니고,

  • 런타임.class 파일을 읽어서

  • 클래스의 메타정보(클래스명, 메서드, 필드, 생성자 등)조사하거나 조작하는 것.

  • 그러니까 "코드를 실행 중"에,

    • 클래스 이름을 알고 있다면

    • 해당 클래스의 메서드, 필드, 생성자 등에 접근해서

    • 동적으로 메서드 호출, 객체 생성, 필드 읽기/수정 등을 할 수 있다.

리플렉션이란, 실행 중인 프로그램 안에서 클래스의 구조(메타데이터)를 조사하고, 그 구조를 기반으로 메서드 호출, 필드 접근, 객체 생성을 동적으로 수행하는 기술이다.


흐름상

  • 컴파일할 때는 그냥 .class 파일만 만들어진다. (메타데이터 포함해서)

  • 실행 중에 (런타임) ClassLoader.class 파일을 JVM 메모리에 로드한다.

  • 리플렉션 API (Class, Method, Field, Constructor 등)를 이용해서
    메모리에 올라온 클래스 정보를 읽고, 필요한 동작(메서드 호출, 필드 변경 등)을 한다.


예를 들어

Class<?> clazz = Class.forName("com.example.MyClass");

// 메서드 조회
Method method = clazz.getDeclaredMethod("myMethod", String.class);

// 객체 생성
Object instance = clazz.getDeclaredConstructor().newInstance();

// 메서드 실행
method.invoke(instance, "argument");

// 필드 접근
Field field = clazz.getDeclaredField("myField");
field.setAccessible(true);
field.set(instance, "newValue");

런타임에 저렇게 코드로 직접 클래스/메서드/필드 조작하는 걸 리플렉션이라고 한다.


정리표

시점 일어나는 일
컴파일 시점 단순히 .class 파일 생성 (메타데이터 저장)
런타임 시점 리플렉션 API를 통해 메타정보를 조회하거나 조작
구분 설명
언제? 런타임(Runtime) 시점
뭘 할 수 있나? 클래스 메타정보 조회, 메서드 호출, 필드 읽기/쓰기, 객체 생성
전제조건 클래스 이름을 알고 있어야 함 (또는 Class 객체를 얻을 수 있어야 함)
주의사항 성능 저하 가능성 있음, 타입 안정성이 떨어질 수 있음



1. 런타임에 메모리에 올라간 클래스 구조 (ClassLoader + 메타영역)

1-1. 클래스가 메모리에 올라가는 과정

클래스 하나가 실행 중에 메모리에 올라오려면 이 과정을 거친다:

1. 클래스 로딩 (Loading)
2. 클래스 검증 (Verification)
3. 클래스 준비 (Preparation)
4. 클래스 초기화 (Initialization)
5. 사용 가능 (ready!)
단계 설명
1. Loading .class 파일을 읽어서 JVM 메모리에 올린다. (ClassLoader 담당)
2. Verification 읽은 클래스 파일이 유효한지 검증한다. (형식, 보안 검사 등)
3. Preparation static 필드들을 메모리에 만든다. (초기값은 default)
4. Initialization static 블록, static 필드 초기화 코드 실행
5. Ready 이제 진짜로 사용 가능

1-2. 누가 클래스 로딩하는가? => ClassLoader

  • JVM에는 여러 종류의 ClassLoader가 있다.
ClassLoader 설명
Bootstrap ClassLoader 가장 기본, JDK 자체 코어 클래스 로드 (ex: java.lang.String)
Extension ClassLoader JDK의 확장 라이브러리 로드
Application ClassLoader 내가 작성한 프로젝트 코드를 로드 (우리가 주로 여기에 관심 있음)

내가 만든 MyClass.class 파일
Application ClassLoader가 읽어서 메모리에 올린다.


1-3. 메타영역(Metaspace)

  • 자바 8 이후부터는,
    클래스에 대한 정보 (메서드, 필드, 이름, 상속구조 등)
    전부 Metaspace 라는 메모리 영역에 저장되는데,

자바 7까지는 PermGen이라는 영역에 저장했는데,
자바 8부터는 PermGen이 사라지고 Metaspace로 대체됨.


Metaspace 특징:

  • JVM 힙(heap)이 아니라 OS의 메모리(native memory) 를 사용한다.

  • 그래서 PermGen처럼 OOM(OutOfMemoryError) 발생 위험이 줄었다.

  • 클래스가 언로드(메모리에서 제거)되지 않으면 Metaspace도 계속 커질 수 있음.

Metaspace는 런타임에 "클래스가 어떤 구조를 가지고 있는지(설계도)"를 저장하는 공간이다.
프로젝트 전체 메타데이터가 여기에 쌓이지만, 실제 인스턴스 데이터는 Heap에 저장된다.


정리 흐름

[.class 파일]
    ↓ (Application ClassLoader)
[클래스 로딩]
    ↓
[메타정보 → Metaspace에 저장]
    ↓
[클래스 인스턴스 → JVM 힙에 생성 가능]

📌 2. 리플렉션이 왜 느린가?

2-1. 일반적인 메서드 호출 vs 리플렉션 메서드 호출

방식 설명
일반 호출 (obj.method()) 컴파일러가 미리 호출할 메서드를 확정. JVM이 직접 빠르게 실행.
리플렉션 호출 (method.invoke(obj, ...)) 런타임에 메서드를 찾아야 하고, 접근 권한 체크, 파라미터 변환, 에러 처리 등이 필요함.

2-2. 리플렉션은 "느린 이유" 요약

  • 런타임 타입 검사:

    • 메서드 호출 전에 매번 타입 체크를 해야 함.
  • 접근 제어 처리:

    • private 메서드/필드라면, setAccessible(true)를 통해 접근 가능해야 함. (보안 검사 추가)
  • 동적 호출 처리:

    • 메서드 오버로딩(같은 이름 다른 파라미터) 처리 필요
  • 예외 처리:

    • InvocationTargetException, IllegalAccessException 등 다양한 예외를 감싸야 함.
  • JVM 최적화 불가:

    • 일반 호출은 JIT 컴파일러가 인라인 최적화(inline optimization) 하는데,

    • 리플렉션은 그걸 못 해서 무조건 풀 프로세스를 밟는다.


결론 요약

주제 한 줄 요약
런타임 클래스 구조 클래스는 ClassLoader가 메모리에 올리고, 메타정보는 Metaspace에 저장
리플렉션 성능 이슈 런타임에 동적 타입체크, 보안검사, 예외처리 등이 추가되기 때문에 기본 메서드 호출보다 훨씬 느리다



1. Class.forName() 은 어떻게 메모리에서 클래스를 찾는가?

1-1. 기본 개념

Class<?> clazz = Class.forName("com.example.MyClass");

하면 JVM은 이 문자열("com.example.MyClass")을 가지고
해당 클래스를 메모리에 로드하거나, 이미 로드된 걸 찾아서 반환한다.


1-2. 내부 흐름

클래스 로딩 로직은 이렇게 진행돼:

1. 현재 쓰레드의 ContextClassLoader를 가져온다.
2. 주어진 클래스 이름("com.example.MyClass")을 이용해 클래스 탐색 시작.
3. 이미 JVM 메모리에 로드된 클래스인지 확인.
   → 있으면 바로 반환
4. 없으면 ClassLoader를 이용해 .class 파일을 찾아 로딩 시도.
5. 찾으면 로드 + 메타데이터(Metaspace 저장) + Class 객체 생성.
6. Class 객체 반환.

1-3. 구체적인 과정

부트스트랩 클래스 로더확장 클래스 로더애플리케이션 클래스 로더
순서로 올라가면서 클래스를 찾는 "부모 위임 모델 (Parent Delegation Model)" 을 사용한다.

(즉,
먼저 부모한테 물어보고 부모가 못 찾으면 자기가 직접 로드하는 방식)

왜 이렇게 하냐?
→ 같은 이름의 다른 클래스를 엉뚱하게 덮어쓰는 걸 막기 위해. (보안)


1-4. 예외 상황

  • 클래스가 없으면 ClassNotFoundException 던진다.

  • 접근이 불가능한 클래스면 IllegalAccessException 날 수도 있다.


요약

Class.forName()현재 쓰레드의 ClassLoader를 통해,
부모부터 차례로 올라가면서
메모리에 이미 로드된 클래스가 있나 찾고, 없으면 로딩 후 메타스페이스에 저장하고 Class<?> 객체를 리턴하는 것이다.


2. Method.invoke() 내부는 어떻게 돌아가는가?

2-1. 기본 개념

method.invoke(object, args);

하면
==런타임에 메서드를 찾아서 실행하는 것.==


2-2. 내부 흐름

1. method.invoke를 호출하면
2. 접근 가능한지 검사 (private면 setAccessible(true) 했는지 체크)
3. 전달된 파라미터(args)를 메서드 파라미터 타입에 맞게 검증
4. 메서드를 실제로 호출 (네이티브 호출, JNI)
5. 리턴 값 있으면 반환
6. 예외 발생 시 InvocationTargetException에 감싸서 던짐
  • 1) 메서드 메타데이터를 읽는다
    (ex. 매개변수 타입, 리턴 타입 등)

  • 2) 네이티브 코드 호출
    (JNI 기반으로 C언어 레벨에서 JVM 내부 메서드 실행 요청)

  • 3) 접근 제어

    • private 메서드라면 접근 가능한지 확인.

    • AccessibleObject 클래스를 통해 제어 (Reflection API 내부 로직)

  • 4) 인자 타입 강제 변환

    • 예를 들어, 메서드 파라미터가 int인데, 넘긴 인자가 Integer라면 변환 처리.
  • 5) 예외 처리

    • 호출된 메서드가 예외를 던지면 무조건 InvocationTargetException으로 감싸서 던짐.

2-4. 왜 느리냐?

  • 매번 타입 체크해야 한다.

  • 메서드 찾고 접근 검사해야 한다.

  • 네이티브(JNI)로 메서드를 호출한다. (느림)

  • 최적화(JIT 인라이닝) 안된다.


최종 정리

항목 설명
Class.forName() ClassLoader를 통해 클래스를 찾고 없으면 메모리에 로딩 후 Class 객체 생성
Method.invoke() 런타임에 메타데이터로 메서드 타입검사 후 네이티브로 메서드를 호출, 예외를 감싸서 반환

요약

Class.forName()은 ClassLoader 체인을 따라 클래스를 찾고 메모리에 올리는 동작,
Method.invoke()는 런타임에 메서드 메타정보 기반으로 타입 체크 → 접근 권한 확인 → 네이티브 호출 → 예외처리까지 수행하는 복잡한 동적 호출이다.


[Class.forName("com.example.MyClass")]
    ↓
[ApplicationClassLoader가 com/example/MyClass.class 찾아서 읽음]
    ↓
[Metaspace에 클래스 메타정보 저장]
    ↓
[Class<?> 객체 생성]

↓

[Method method = clazz.getDeclaredMethod("sayHello")]
[method.invoke(instance)]
    ↓
[접근성 검사 → 타입 검사 → 네이티브 호출 → 결과 반환]