2주차 Unit 6.1 — Reflection의 본질

Psj·2026년 5월 18일

F-lab

목록 보기
73/230

Unit 6.1 — Reflection의 본질

F-LAB JAVA · 2주차 · Phase 6 · Reflection & Iterator
🚀 Phase 6 시작 — 정적 언어에 동적 능력을 부여하는 마법


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • Reflection 의 정확한 정의는?
  • 자바가 컴파일 타임에 모든 타입을 정한다고 하는데, 어떻게 Reflection이 가능한가?
  • Class<?> 객체 의 정체는?
  • Reflection 으로 무엇을 할 수 있는가?
  • Reflection 의 성능 비용 은 얼마인가?
  • Spring, JPA, Jackson이 Reflection 을 어떻게 활용하나?
  • ILIC에서 Reflection 을 직접 써야 할 순간이 있나?

🎯 핵심 한 문장

Reflection은 "프로그램이 자기 자신의 구조를 들여다보고 조작하는 능력"이다.
컴파일 타임에 정적으로 정해진 자바에서, 런타임에 클래스/메서드/필드를 동적으로 다룰 수 있게 해주는 메커니즘.
Phase 3 (바이트코드) + Phase 2 (메서드 호출) 의 메모리 구조 위에서 이뤄진다.
Spring, JPA, Mockito 등 모든 현대 자바 프레임워크의 근간.

비유 — 자기 모습을 거울에 비추기

시스템비유
일반 객체보통 사람 — 자기 얼굴을 직접 못 봄
Reflection거울 들고 자기 모습 관찰
Class<?> 객체거울에 비친 모습 (메타 정보)
Method 객체거울로 본 자기 메서드 목록
Field 객체거울로 본 자기 필드
method.invoke()거울 속 자기에게 행동 시키기

→ 평소엔 직접 동작하지만, 거울 통해 자기 자신을 조작할 수도 있음.


🧭 9개 섹션 로드맵

1. Reflection이란 무엇인가
2. Class 객체의 정체
3. Reflection으로 할 수 있는 일
4. Class 객체 얻는 4가지 방법
5. Field, Method, Constructor 다루기
6. 성능 비용 — Reflection이 느린 이유
7. ILIC 실무 — 프레임워크의 동작
8. 흔한 실수 + 보안
9. 면접 질문 + 자기 점검

1️⃣ Reflection이란 무엇인가

1.1 정의

Reflection (반사):

  • 프로그램이 자기 자신의 구조와 동작을 검사·조작할 수 있는 능력
  • 런타임에 클래스/메서드/필드 정보를 동적으로 다룸

1.2 정적 vs 동적

// 정적 (Static) — 일반적인 코드
Shipment s = new Shipment();
s.calculate(100);

// 컴파일 타임에 모든 게 결정:
// - Shipment 클래스
// - 생성자
// - calculate 메서드
// - 매개변수 타입 int
// 동적 (Dynamic) — Reflection
Class<?> clazz = Class.forName("com.ilic.Shipment");
Object obj = clazz.getDeclaredConstructor().newInstance();
Method m = clazz.getMethod("calculate", int.class);
m.invoke(obj, 100);

// 런타임에 결정:
// - 어느 클래스를 사용할지 (문자열로)
// - 어느 메서드를 부를지 (이름으로)
// - 매개변수는 무엇인지

컴파일러는 Shipment, calculate, 100을 전혀 모름.
→ 런타임에 모두 결정.

1.3 왜 가능한가 — Phase 3 복습

박승제씨가 Phase 3에서 본 것:

  • .class 파일에는 Constant Pool에 모든 메타 정보 보존
  • 클래스명, 메서드명, 필드명, 시그니처가 Utf8 항목으로 그대로 존재
  • 클래스 로딩 시 Method Area에 적재됨

→ JVM은 항상 이 정보를 메모리에 가지고 있음.
→ Reflection은 그 정보를 자바 코드에서 접근할 수 있게 해줌.

1.4 자기 점검 답변

자바는 정적 타입 언어인데, 어떻게 동적으로 메서드를 부를 수 있는가?

:
1. .class 파일에 메타 정보 보존 (Phase 3)
2. 클래스 로딩 시 Method Area에 적재
3. Reflection API가 그 정보에 접근 가능
4. Method 객체를 통해 invoke 호출 → JVM이 실제 메서드 실행

→ "정적 타입"은 컴파일러의 검증을 의미.
→ JVM 런타임에는 모든 메타 정보가 살아있어서 동적 호출 가능.

1.5 Reflection의 4가지 능력

1. 클래스 탐색
   - 클래스 정보 (이름, 부모, 인터페이스)
   - 멤버 목록 (필드, 메서드, 생성자)

2. 객체 생성
   - new 없이 인스턴스 생성

3. 메서드 호출
   - 이름으로 메서드 호출

4. 필드 접근
   - private이라도 읽고 쓰기 가능

→ "거의 모든 일을 동적으로 할 수 있음".


2️⃣ Class 객체의 정체

2.1 Class 객체 — Reflection의 출발점

Class<?> clazz = Shipment.class;

Class 객체는 모든 클래스의 메타 정보를 담은 객체.

2.2 메모리에서의 위치

Unit 1.3에서 본 것 복습:

Method Area:
  Shipment Klass (JVM 내부 구조체)
    - 클래스 메타데이터
    - 메서드 바이트코드
    - static 필드 등

Heap:
  Class<Shipment> 객체   ← Reflection이 접근하는 곳
    - Klass를 가리키는 참조
    - 자바 코드에서 다룰 수 있는 인터페이스

Class<?>Heap의 자바 객체, JVM 내부 Klass의 자바 인터페이스.

2.3 Class 객체의 주요 메서드

Class<Shipment> clazz = Shipment.class;

// 클래스 정보
clazz.getName();           // "com.ilic.Shipment"
clazz.getSimpleName();     // "Shipment"
clazz.getPackage();        // Package 객체
clazz.getSuperclass();     // Class<Object> 또는 부모
clazz.getInterfaces();     // Class[] (구현 인터페이스)

// 필드
clazz.getFields();         // public 필드만
clazz.getDeclaredFields(); // 모든 필드 (private 포함)

// 메서드
clazz.getMethods();         // public 메서드 (상속 포함)
clazz.getDeclaredMethods(); // 자기 클래스의 모든 메서드

// 생성자
clazz.getConstructors();
clazz.getDeclaredConstructors();

// 어노테이션
clazz.getAnnotations();
clazz.isAnnotationPresent(Entity.class);

// 검사
clazz.isInterface();
clazz.isEnum();
clazz.isArray();
clazz.isPrimitive();

2.4 Class 객체는 단 1개

Class<?> c1 = Shipment.class;
Class<?> c2 = Class.forName("com.ilic.Shipment");
Class<?> c3 = new Shipment().getClass();

c1 == c2;   // true
c2 == c3;   // true

각 클래스마다 Heap에 Class 객체가 정확히 1개.
→ ClassLoader 별로는 별개일 수 있음 (Unit 3.3).

2.5 제네릭 타입 정보

Class<Shipment> clazz1 = Shipment.class;
Class<? extends Shipment> clazz2 = DryShipment.class;
Class<?> clazz3 = unknownClass();

<?> = "어떤 타입인지 모른다"
<? extends Shipment> = "Shipment 또는 그 자식"
<Shipment> = "Shipment 자체"

타입 안전성을 위한 표기. 런타임 동작에는 영향 없음 (Type Erasure).


3️⃣ Reflection으로 할 수 있는 일

3.1 클래스 정보 탐색

Class<Shipment> clazz = Shipment.class;

// 클래스 이름과 패키지
System.out.println(clazz.getName());          // com.ilic.Shipment
System.out.println(clazz.getPackage());        // com.ilic

// 모든 필드 (private 포함)
for (Field f : clazz.getDeclaredFields()) {
    System.out.println(f.getName() + " : " + f.getType());
}
// 출력:
// id : class java.lang.Long
// blNo : class java.lang.String
// weight : int
// ...

3.2 객체 동적 생성

// new 없이 객체 만들기
Class<?> clazz = Class.forName("com.ilic.Shipment");

// 기본 생성자
Object obj = clazz.getDeclaredConstructor().newInstance();

// 인자 있는 생성자
Constructor<?> con = clazz.getConstructor(String.class, LocalDate.class);
Object s = con.newInstance("BL-001", LocalDate.now());

→ Spring, JUnit, Jackson 등이 사용하는 메커니즘.

3.3 메서드 동적 호출

Shipment s = new Shipment();
Class<?> clazz = s.getClass();

// 메서드 객체 얻기
Method m = clazz.getMethod("calculate", int.class);

// 호출
Object result = m.invoke(s, 100);   // s.calculate(100)과 동일

→ AOP, RPC, 동적 프록시의 기반.

3.4 필드 직접 접근 (private 포함)

Shipment s = new Shipment("BL-001");
Class<?> clazz = s.getClass();

Field f = clazz.getDeclaredField("blNo");
f.setAccessible(true);   // private 접근 허용

// 읽기
String blNo = (String) f.get(s);

// 쓰기
f.set(s, "BL-999");

→ ORM (JPA), 테스트 라이브러리(Mockito)가 사용.
setAccessible(true) 가 핵심.

3.5 어노테이션 처리

@Entity
@Table(name = "shipments")
public class Shipment {
    @Id
    @GeneratedValue
    private Long id;
    
    @Column(name = "bl_no")
    private String blNo;
}

// Reflection으로 어노테이션 읽기
Class<Shipment> clazz = Shipment.class;
if (clazz.isAnnotationPresent(Entity.class)) {
    Table table = clazz.getAnnotation(Table.class);
    System.out.println(table.name());   // "shipments"
}

for (Field f : clazz.getDeclaredFields()) {
    if (f.isAnnotationPresent(Id.class)) {
        System.out.println("Primary key: " + f.getName());
    }
}

→ JPA, Spring, Lombok 등 거의 모든 프레임워크가 사용.

3.6 배열 동적 다루기

import java.lang.reflect.Array;

// 동적으로 배열 생성
int[] arr = (int[]) Array.newInstance(int.class, 10);

// 동적으로 배열 접근
Array.setInt(arr, 0, 100);
int value = Array.getInt(arr, 0);

→ 거의 안 씀. 라이브러리 내부용.

3.7 동적 프록시 (Unit 2.4와 연결)

// JDK 표준 동적 프록시
ShipmentService proxy = (ShipmentService) Proxy.newProxyInstance(
    ShipmentService.class.getClassLoader(),
    new Class[] { ShipmentService.class },
    (proxyObj, method, args) -> {
        log.info("Before " + method.getName());
        Object result = method.invoke(realObj, args);
        log.info("After " + method.getName());
        return result;
    }
);

→ Spring AOP의 단순화 버전.
→ 인터페이스 기반 프록시.


4️⃣ Class 객체 얻는 4가지 방법

4.1 방법 1 — 클래스 리터럴

Class<Shipment> clazz = Shipment.class;

가장 권장.

  • 컴파일 타임 검증 (클래스 없으면 컴파일 에러)
  • 빠름 (이미 메모리에 있는 참조 가져옴)

4.2 방법 2 — Class.forName(name)

Class<?> clazz = Class.forName("com.ilic.Shipment");

문자열로 클래스명 지정.

  • 클래스 로딩 발동 (안 됐다면)
  • 클래스 없으면 ClassNotFoundException
  • 가장 동적
// 예외 처리 필요
try {
    Class<?> clazz = Class.forName(userInput);
    // ...
} catch (ClassNotFoundException e) {
    log.error("Class not found: " + userInput);
}

4.3 방법 3 — obj.getClass()

Shipment s = new Shipment();
Class<? extends Shipment> clazz = s.getClass();

인스턴스로부터.

  • 실제 객체의 클래스 (동적 타입)
  • 다형성 활용
Cargo c = new DryCargo();   // 정적 타입 Cargo, 동적 타입 DryCargo
c.getClass();   // class com.ilic.DryCargo  ← 실제 타입

4.4 방법 4 — ClassLoader

ClassLoader cl = Shipment.class.getClassLoader();
Class<?> clazz = cl.loadClass("com.ilic.OtherClass");

고급 사용.

  • 특정 ClassLoader 사용
  • 인스턴스화는 안 함 (forName과 다른 점)
  • 격리 환경에서 유용

4.5 4가지 방법 비교

방법사용 시점클래스 로딩예외
Shipment.class컴파일 타임 확정이미 로딩됨없음
Class.forName(name)런타임 결정로딩 + 초기화ClassNotFoundException
obj.getClass()인스턴스 보유이미 로딩됨없음
cl.loadClass(name)특정 CL 지정로딩만ClassNotFoundException

4.6 ILIC에서의 선택

// 가장 흔함: 클래스 리터럴
Class<Shipment> clazz = Shipment.class;

// 동적 클래스명: Class.forName
Class<?> entityClass = Class.forName(config.getEntityType());

// 인스턴스 기반: obj.getClass()
public <T> Class<T> typeOf(T obj) {
    return (Class<T>) obj.getClass();
}

박승제씨가 직접 Reflection 쓸 일은 드물지만, 알아둬야 함.


5️⃣ Field, Method, Constructor 다루기

5.1 Field 객체

Class<Shipment> clazz = Shipment.class;
Field f = clazz.getDeclaredField("blNo");

// 정보 조회
f.getName();              // "blNo"
f.getType();              // Class<String>
f.getModifiers();          // public/private 등의 비트마스크
f.isAnnotationPresent(...);

// private 접근 허용
f.setAccessible(true);

// 인스턴스 필드 읽기/쓰기
Shipment s = new Shipment();
String value = (String) f.get(s);
f.set(s, "BL-999");

// static 필드는 null 전달
Field staticField = clazz.getDeclaredField("DEFAULT_RATE");
BigDecimal rate = (BigDecimal) staticField.get(null);

5.2 Method 객체

Class<Shipment> clazz = Shipment.class;
Method m = clazz.getMethod("calculate", int.class);

// 정보 조회
m.getName();              // "calculate"
m.getReturnType();        // Class<BigDecimal>
m.getParameterTypes();    // [int.class]
m.getDeclaringClass();    // Class<Shipment>
m.getModifiers();
m.isVarArgs();

// 호출
Shipment s = new Shipment();
Object result = m.invoke(s, 100);   // → BigDecimal

5.3 Constructor 객체

Class<Shipment> clazz = Shipment.class;

// 기본 생성자
Constructor<Shipment> con0 = clazz.getDeclaredConstructor();

// 인자 있는 생성자
Constructor<Shipment> con1 = clazz.getDeclaredConstructor(String.class, LocalDate.class);

// 객체 생성
Shipment s0 = con0.newInstance();
Shipment s1 = con1.newInstance("BL-001", LocalDate.now());

5.4 getXxx vs getDeclaredXxx

class Parent {
    public String parentField;
    public void parentMethod() {}
    private String parentSecret;
}

class Child extends Parent {
    public String childField;
    private String childSecret;
}

Class<Child> clazz = Child.class;

// getXxx: public + 상속 받은 멤버
clazz.getFields();
// → childField, parentField (모두 public, 상속 포함)

clazz.getMethods();
// → childMethod, parentMethod, Object의 메서드들

// getDeclaredXxx: 모든 접근자 + 자기 클래스만
clazz.getDeclaredFields();
// → childField, childSecret (private 포함, 상속 X)

clazz.getDeclaredMethods();
// → 자기 클래스 메서드만
메서드접근자상속
getFieldspublic만상속 포함
getDeclaredFields모든 접근자자기 클래스만
getMethodspublic만상속 포함
getDeclaredMethods모든 접근자자기 클래스만

5.5 setAccessible의 의미

// 기본: private 멤버 접근 불가
Field f = clazz.getDeclaredField("blNo");
f.get(obj);   // ❌ IllegalAccessException

// setAccessible(true)로 해제
f.setAccessible(true);
f.get(obj);   // ✓ 접근 가능

setAccessible의 역할:

  • 자바의 접근 제어 (private, protected) 우회
  • 보안 매니저가 허용해야 가능 (대부분 허용)
  • 위험한 작업 — 캡슐화 깨짐

→ ORM, 직렬화 등이 사용하는 이유.

5.6 Java 9+의 모듈 시스템

// Java 9+에서 default 패키지 접근 제한
Field f = SomeClass.class.getDeclaredField("internal");
f.setAccessible(true);   // ❌ InaccessibleObjectException

해결:

# JVM 옵션
--add-opens java.base/java.util=ALL-UNNAMED

→ Java 9+ 모듈 시스템과 충돌.
→ 일부 라이브러리는 이 옵션 필요.


6️⃣ 성능 비용 — Reflection이 느린 이유

6.1 일반 호출 vs Reflection 호출

// 일반 호출
shipment.calculate(100);
// 바이트코드: invokevirtual
// JIT 인라이닝 가능 → 거의 0 비용

// Reflection 호출
Method m = clazz.getMethod("calculate", int.class);
m.invoke(shipment, 100);
// 1. getMethod: 메서드 검색 비용
// 2. invoke: 매개변수 박싱/언박싱
// 3. 보안 검사
// 4. 실제 호출

6.2 Reflection의 추가 비용

1. 메서드 검색 (getMethod)
   - 첫 호출 시 비용 큼
   - 캐시되면 재호출은 빠름

2. 보안 검사
   - 매 invoke마다 (setAccessible 안 했다면)
   - 작은 비용이지만 누적

3. 매개변수 박싱
   - primitive → Object[] 변환
   - int → Integer

4. JIT 최적화 어려움
   - invoke 호출은 동적이라 인라이닝 어려움
   - 최근 JVM은 invokedynamic으로 일부 개선

6.3 측정 (JMH 벤치마크, 가상)

일반 호출:      1 ns
Reflection (cold):  500 ns
Reflection (warm):  50 ns
Reflection + setAccessible:  20 ns

핵심:

  • Reflection은 일반 호출의 20~500배 느림
  • 그러나 절대적 시간으론 여전히 빠름 (ns 단위)
  • 반복 사용 시 캐시로 단축

6.4 캐싱 패턴

// ❌ 매번 getMethod
for (int i = 0; i < 1000; i++) {
    Method m = clazz.getMethod("calculate", int.class);   // 매번 검색
    m.invoke(obj, i);
}

// ✓ 한 번만 getMethod
Method m = clazz.getMethod("calculate", int.class);
m.setAccessible(true);   // 보안 검사도 우회
for (int i = 0; i < 1000; i++) {
    m.invoke(obj, i);
}

Method 객체를 캐시하면 수십 배 빠름.

6.5 Java 7+의 MethodHandle

// 더 빠른 동적 호출
MethodHandle handle = MethodHandles.lookup()
    .findVirtual(Shipment.class, "calculate", MethodType.methodType(BigDecimal.class, int.class));

BigDecimal result = (BigDecimal) handle.invokeExact(shipment, 100);

MethodHandle:

  • Reflection보다 빠름
  • invokedynamic 명령 활용
  • 람다의 기반

→ 고성능 동적 호출이 필요하면 MethodHandle.
→ 일반적으론 Reflection으로 충분.

6.6 자기 점검 답변

Reflection은 정말 느린가?

:

  • 일반 호출보다 느림: 20~500배
  • 하지만 절대 시간으로는 여전히 빠름 (ns 단위)
  • 반복 사용 시 캐시로 큰 폭 단축
  • Spring/JPA는 거의 모든 동작에 Reflection 사용 → 실용적으로 문제 없음
  • 진짜 hot path만 신경 쓰면 됨

7️⃣ ILIC 실무 — 프레임워크의 동작

7.1 Spring DI의 비밀

@Service
public class ShipmentService {
    private final ShipmentRepository repository;
    
    public ShipmentService(ShipmentRepository repository) {
        this.repository = repository;
    }
}

Spring이 하는 일:

1. Class.forName("com.ilic.ShipmentService")
2. Constructor[] cons = clazz.getDeclaredConstructors()
3. 매개변수 타입 확인: [ShipmentRepository]
4. ShipmentRepository 빈 조회 (없으면 재귀적 생성)
5. constructor.newInstance(repositoryBean)
6. 결과 객체를 ApplicationContext에 등록

→ Spring의 DI는 Reflection의 정수.

7.2 @Autowired 필드 주입

@Service
public class ShipmentService {
    @Autowired
    private ShipmentRepository repository;
}

Spring 동작:

1. clazz.getDeclaredFields()
2. for each field:
   3. field.isAnnotationPresent(Autowired.class)?
   4. Yes → field.setAccessible(true)
   5. field.set(serviceInstance, repositoryBean)

private field에 setAccessible로 주입.

7.3 JPA Entity 매핑

@Entity
public class Shipment {
    @Id
    private Long id;
    
    @Column(name = "bl_no")
    private String blNo;
}

Hibernate 동작:

1. 클래스 스캔: @Entity 어노테이션 찾기
2. clazz.getDeclaredFields():
   - id: @Id → primary key
   - blNo: @Column(name="bl_no") → 컬럼 매핑
3. SQL 생성: SELECT id, bl_no FROM shipments
4. ResultSet 결과를 Reflection으로 객체에 주입:
   - field.setAccessible(true)
   - field.set(shipmentInstance, resultSet.getValue())

→ JPA의 매핑은 Reflection 기반.

7.4 Jackson JSON 직렬화

public class Shipment {
    private Long id;
    private String blNo;
}

ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(shipment);
// {"id": 1, "blNo": "BL-001"}

Jackson 동작:

1. clazz.getDeclaredFields()
2. for each field:
   3. field.setAccessible(true)
   4. value = field.get(obj)
   5. JSON에 "fieldName": value 추가

역직렬화:

1. JSON 파싱
2. clazz.getDeclaredConstructor().newInstance()
3. for each json property:
   4. clazz.getDeclaredField(name)
   5. field.set(obj, parsedValue)

7.5 Mockito 모의 객체

@Test
void test() {
    ShipmentRepository mockRepo = Mockito.mock(ShipmentRepository.class);
    when(mockRepo.findById(1L)).thenReturn(Optional.of(testShipment));
}

Mockito 동작:
1. Class.forName 또는 clazz로 인터페이스 정보 획득
2. CGLIB 또는 ByteBuddy로 새 클래스 동적 생성
3. Unsafe.allocateInstance 로 인스턴스 생성 (생성자 우회)
4. Reflection으로 메서드 호출 감지 → 정의된 동작 반환

→ Reflection + 동적 클래스 생성 + Unsafe.

7.6 Lombok의 @Data

@Data
public class Shipment {
    private Long id;
    private String blNo;
}
// 컴파일 시:
// - getId(), getBlNo() 자동 생성
// - setId(), setBlNo() 자동 생성
// - equals, hashCode, toString 자동 생성

Lombok 동작:
1. 컴파일 타임 어노테이션 처리
2. AST(Abstract Syntax Tree) 조작
3. 바이트코드에 메서드 추가

Reflection은 아니지만:

  • 어노테이션 처리기는 Class/Field/Method 정보 다룸 (compile-time reflection)
  • 비슷한 메커니즘

7.7 박승제씨가 ILIC에서 만나는 Reflection

직접 사용: 거의 없음

간접 사용 (프레임워크 통해):
  - Spring: 모든 DI/AOP
  - JPA/Hibernate: 모든 매핑
  - Jackson: 모든 JSON
  - Spring Validation: 어노테이션 처리
  - JUnit: 테스트 메서드 찾기

→ 박승제씨는 Reflection을 "쓰는" 게 아니라 "이해해야" 함
→ 프레임워크 동작 디버깅 시 도움

8️⃣ 흔한 실수 + 보안

실수 1 — Reflection 비용 무시

// ❌ 매 호출마다 getMethod
public void process(Object obj) {
    Method m = obj.getClass().getMethod("calculate", int.class);
    m.invoke(obj, 100);
}

해결: Method 캐시.

private static final Map<Class<?>, Method> METHOD_CACHE = new ConcurrentHashMap<>();

public void process(Object obj) {
    Method m = METHOD_CACHE.computeIfAbsent(obj.getClass(),
        c -> { try { return c.getMethod("calculate", int.class); }
               catch (NoSuchMethodException e) { throw new RuntimeException(e); } });
    m.invoke(obj, 100);
}

실수 2 — setAccessible 남발

// ❌ 모든 필드 setAccessible
for (Field f : obj.getClass().getDeclaredFields()) {
    f.setAccessible(true);
    // ...
}

문제:

  • 캡슐화 깨짐
  • 보안 이슈
  • Java 9+ 모듈 충돌

→ 정말 필요할 때만.

실수 3 — 동적 클래스명 신뢰

// ❌ 사용자 입력을 그대로
@PostMapping("/dynamic")
public Object dynamic(@RequestParam String className) {
    Class<?> clazz = Class.forName(className);   // 위험!
    return clazz.getDeclaredConstructor().newInstance();
}

문제:

  • 임의 클래스 인스턴스화 가능
  • 보안 취약점
  • Java deserialize 취약점과 유사

해결:

  • 화이트리스트
  • 사용자 입력 검증
  • 허용된 클래스만 인스턴스화

실수 4 — 예외 처리 누락

// ❌
Class<?> clazz = Class.forName(name);   // ClassNotFoundException
Method m = clazz.getMethod("method");    // NoSuchMethodException
m.invoke(obj);                           // InvocationTargetException, IllegalAccessException

해결:

try {
    Class<?> clazz = Class.forName(name);
    Method m = clazz.getMethod("method");
    m.invoke(obj);
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
    log.error("Reflection failed", e);
} catch (InvocationTargetException e) {
    Throwable cause = e.getCause();   // 실제 메서드 내부 예외
    log.error("Method threw", cause);
}

특히 InvocationTargetException 의 cause 처리 중요.

실수 5 — 제네릭 정보 다루기

List<Shipment> list = ...;
list.getClass();   // class java.util.ArrayList — 제네릭 정보 X

런타임에 제네릭 타입 정보 거의 사라짐 (Type Erasure).

해결:

  • ParameterizedType 사용
  • 또는 명시적 Class 전달
public <T> List<T> findAll(Class<T> type) {
    // ...
}

실수 6 — final 필드 변경 시도

public class Config {
    private static final String VERSION = "1.0";
}

Field f = Config.class.getDeclaredField("VERSION");
f.setAccessible(true);
f.set(null, "2.0");   // 일부 JVM에서 성공하지만 매우 위험

문제:

  • JIT가 final로 가정해 인라이닝
  • 변경해도 컴파일된 코드는 옛 값 사용
  • 예측 불가능한 동작

→ final 필드는 절대 변경 X.

보안 — Reflection의 위험

Reflection은 캡슐화를 깰 수 있음:
  - private 필드 접근
  - 임의 메서드 호출
  - 임의 객체 생성

위험:
  - 악성 라이브러리가 시스템 정보 빼냄
  - 신뢰할 수 없는 코드 실행
  - 보안 우회

자바의 대응:
  - SecurityManager (deprecated, 곧 제거)
  - Java 9+ 모듈 시스템 (--add-opens)
  - 단계적으로 Reflection 제한

디버깅 도구

# Reflection 호출 추적 (디버그 옵션)
java -Dsun.reflect.debugInflation=true App

# 어떤 클래스가 어떤 어노테이션 가지나
# IntelliJ → Run → "Show Annotations"

9️⃣ 면접 질문 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
Reflection이란?런타임에 클래스 구조 검사/조작
Reflection이 가능한 이유?.class 파일의 메타정보, Method Area 적재
Class 객체의 정체?JVM Klass의 자바 인터페이스
Class 객체 얻는 방법?클래스 리터럴, forName, getClass, ClassLoader
getXxx vs getDeclaredXxx?public+상속 vs 모든+자기 클래스
setAccessible의 의미?private 접근 허용. 캡슐화 우회
Reflection의 성능?일반 호출의 20-500배. 캐시로 단축
Spring DI 메커니즘?Reflection으로 생성자/필드 주입
JPA의 매핑?Reflection으로 ResultSet → 필드
InvocationTargetException?invoke 중 발생한 예외 래핑

9.2 자기 점검 체크리스트

기본 이해

  • Reflection의 정의를 안다
  • Class 객체의 정체를 설명할 수 있다
  • Class 얻는 4가지 방법을 안다
  • Field/Method/Constructor 다루기를 안다
  • setAccessible의 의미를 안다

실전 적용

  • Method 객체 캐시 패턴 적용 가능
  • Spring DI 내부 동작을 안다
  • JPA Hibernate의 Reflection 사용을 안다
  • InvocationTargetException 처리 가능
  • 동적 클래스 인스턴스화 보안 인식

면접 대비 — 5분 답변

  • Reflection의 본질
  • 프레임워크가 사용하는 방식
  • 성능 비용과 캐싱
  • 보안 주의사항
  • MethodHandle 같은 대안

🎯 핵심 요약 — 3줄 정리

1. Reflection = 런타임 자기 검사/조작

  • .class의 메타정보 + Method Area 적재
  • Class 객체 = JVM Klass의 자바 인터페이스
  • 클래스/필드/메서드 동적 다루기

2. 4가지 능력

  • 클래스 탐색
  • 객체 생성 (new 없이)
  • 메서드 호출 (이름으로)
  • 필드 접근 (private 포함)

3. ILIC 실무

  • 직접 사용: 거의 없음
  • 간접 사용: Spring/JPA/Jackson/Mockito 모두
  • 이해 필요: 프레임워크 동작 디버깅
  • 성능: 캐시로 충분히 빠름

📚 다음으로...

Phase 6 진행 + Phase 7

Phase 6의 다음 주제 (Iterator)는 짧게 다루고, Phase 7 (Buffer)도 응용 내용.

박승제씨의 1주차 학습에서 이미 본 부분들이 있어서, Phase 6/7은 통합/간략화 가능.

박승제씨가 원하는 방향:
1. Phase 6 Iterator 패턴 본격
2. Phase 7 Buffer (1주차 NIO와 통합)
3. 또는 2주차 종합 정리 + 다른 학습 진입

Phase 6 진행 상황

🚀 Phase 6 — Reflection & Iterator
  ✅ Unit 6.1 Reflection의 본질 ← 여기
  ⏭ (남은 Iterator 학습은 박승제씨 요청에 따라)

2주차 진행 상황

✅ Phase 1 (1.1 ~ 1.6 완주)
✅ Phase 2 (2.1 ~ 2.4 완주)
✅ Phase 3 (3.1 ~ 3.4 완주, 정점)
✅ Phase 4 (4.1 ~ 4.5 완주, 운영 마스터)
✅ Phase 5 (5.1 ~ 5.4 완주, 자료구조 마스터)
🚀 Phase 6 진행 중
⏭ Phase 7

작성한 2주차 학습자료

누적 24개 Unit
2주차 약 90% 완주
profile
Software Developer

0개의 댓글