F-LAB JAVA · 2주차 · Phase 6 · Reflection & Iterator
🚀 Phase 6 시작 — 정적 언어에 동적 능력을 부여하는 마법
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
Class<?> 객체 의 정체는?Reflection은 "프로그램이 자기 자신의 구조를 들여다보고 조작하는 능력"이다.
컴파일 타임에 정적으로 정해진 자바에서, 런타임에 클래스/메서드/필드를 동적으로 다룰 수 있게 해주는 메커니즘.
Phase 3 (바이트코드) + Phase 2 (메서드 호출) 의 메모리 구조 위에서 이뤄진다.
Spring, JPA, Mockito 등 모든 현대 자바 프레임워크의 근간.
| 시스템 | 비유 |
|---|---|
| 일반 객체 | 보통 사람 — 자기 얼굴을 직접 못 봄 |
| Reflection | 거울 들고 자기 모습 관찰 |
Class<?> 객체 | 거울에 비친 모습 (메타 정보) |
| Method 객체 | 거울로 본 자기 메서드 목록 |
| Field 객체 | 거울로 본 자기 필드 |
| method.invoke() | 거울 속 자기에게 행동 시키기 |
→ 평소엔 직접 동작하지만, 거울 통해 자기 자신을 조작할 수도 있음.
1. Reflection이란 무엇인가
2. Class 객체의 정체
3. Reflection으로 할 수 있는 일
4. Class 객체 얻는 4가지 방법
5. Field, Method, Constructor 다루기
6. 성능 비용 — Reflection이 느린 이유
7. ILIC 실무 — 프레임워크의 동작
8. 흔한 실수 + 보안
9. 면접 질문 + 자기 점검
Reflection (반사):
// 정적 (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을 전혀 모름.
→ 런타임에 모두 결정.
박승제씨가 Phase 3에서 본 것:
.class 파일에는 Constant Pool에 모든 메타 정보 보존→ JVM은 항상 이 정보를 메모리에 가지고 있음.
→ Reflection은 그 정보를 자바 코드에서 접근할 수 있게 해줌.
자바는 정적 타입 언어인데, 어떻게 동적으로 메서드를 부를 수 있는가?
답:
1. .class 파일에 메타 정보 보존 (Phase 3)
2. 클래스 로딩 시 Method Area에 적재
3. Reflection API가 그 정보에 접근 가능
4. Method 객체를 통해 invoke 호출 → JVM이 실제 메서드 실행
→ "정적 타입"은 컴파일러의 검증을 의미.
→ JVM 런타임에는 모든 메타 정보가 살아있어서 동적 호출 가능.
1. 클래스 탐색
- 클래스 정보 (이름, 부모, 인터페이스)
- 멤버 목록 (필드, 메서드, 생성자)
2. 객체 생성
- new 없이 인스턴스 생성
3. 메서드 호출
- 이름으로 메서드 호출
4. 필드 접근
- private이라도 읽고 쓰기 가능
→ "거의 모든 일을 동적으로 할 수 있음".
Class<?> clazz = Shipment.class;
Class 객체는 모든 클래스의 메타 정보를 담은 객체.
Unit 1.3에서 본 것 복습:
Method Area:
Shipment Klass (JVM 내부 구조체)
- 클래스 메타데이터
- 메서드 바이트코드
- static 필드 등
Heap:
Class<Shipment> 객체 ← Reflection이 접근하는 곳
- Klass를 가리키는 참조
- 자바 코드에서 다룰 수 있는 인터페이스
→ Class<?> 는 Heap의 자바 객체, JVM 내부 Klass의 자바 인터페이스.
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();
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).
Class<Shipment> clazz1 = Shipment.class;
Class<? extends Shipment> clazz2 = DryShipment.class;
Class<?> clazz3 = unknownClass();
<?> = "어떤 타입인지 모른다"
<? extends Shipment> = "Shipment 또는 그 자식"
<Shipment> = "Shipment 자체"
타입 안전성을 위한 표기. 런타임 동작에는 영향 없음 (Type Erasure).
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
// ...
// 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 등이 사용하는 메커니즘.
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, 동적 프록시의 기반.
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) 가 핵심.
@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 등 거의 모든 프레임워크가 사용.
import java.lang.reflect.Array;
// 동적으로 배열 생성
int[] arr = (int[]) Array.newInstance(int.class, 10);
// 동적으로 배열 접근
Array.setInt(arr, 0, 100);
int value = Array.getInt(arr, 0);
→ 거의 안 씀. 라이브러리 내부용.
// 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의 단순화 버전.
→ 인터페이스 기반 프록시.
Class<Shipment> clazz = Shipment.class;
가장 권장.
Class<?> clazz = Class.forName("com.ilic.Shipment");
문자열로 클래스명 지정.
ClassNotFoundException// 예외 처리 필요
try {
Class<?> clazz = Class.forName(userInput);
// ...
} catch (ClassNotFoundException e) {
log.error("Class not found: " + userInput);
}
Shipment s = new Shipment();
Class<? extends Shipment> clazz = s.getClass();
인스턴스로부터.
Cargo c = new DryCargo(); // 정적 타입 Cargo, 동적 타입 DryCargo
c.getClass(); // class com.ilic.DryCargo ← 실제 타입
ClassLoader cl = Shipment.class.getClassLoader();
Class<?> clazz = cl.loadClass("com.ilic.OtherClass");
고급 사용.
| 방법 | 사용 시점 | 클래스 로딩 | 예외 |
|---|---|---|---|
Shipment.class | 컴파일 타임 확정 | 이미 로딩됨 | 없음 |
Class.forName(name) | 런타임 결정 | 로딩 + 초기화 | ClassNotFoundException |
obj.getClass() | 인스턴스 보유 | 이미 로딩됨 | 없음 |
cl.loadClass(name) | 특정 CL 지정 | 로딩만 | ClassNotFoundException |
// 가장 흔함: 클래스 리터럴
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 쓸 일은 드물지만, 알아둬야 함.
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);
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
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());
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();
// → 자기 클래스 메서드만
| 메서드 | 접근자 | 상속 |
|---|---|---|
getFields | public만 | 상속 포함 |
getDeclaredFields | 모든 접근자 | 자기 클래스만 |
getMethods | public만 | 상속 포함 |
getDeclaredMethods | 모든 접근자 | 자기 클래스만 |
// 기본: private 멤버 접근 불가
Field f = clazz.getDeclaredField("blNo");
f.get(obj); // ❌ IllegalAccessException
// setAccessible(true)로 해제
f.setAccessible(true);
f.get(obj); // ✓ 접근 가능
setAccessible의 역할:
→ ORM, 직렬화 등이 사용하는 이유.
// Java 9+에서 default 패키지 접근 제한
Field f = SomeClass.class.getDeclaredField("internal");
f.setAccessible(true); // ❌ InaccessibleObjectException
해결:
# JVM 옵션
--add-opens java.base/java.util=ALL-UNNAMED
→ Java 9+ 모듈 시스템과 충돌.
→ 일부 라이브러리는 이 옵션 필요.
// 일반 호출
shipment.calculate(100);
// 바이트코드: invokevirtual
// JIT 인라이닝 가능 → 거의 0 비용
// Reflection 호출
Method m = clazz.getMethod("calculate", int.class);
m.invoke(shipment, 100);
// 1. getMethod: 메서드 검색 비용
// 2. invoke: 매개변수 박싱/언박싱
// 3. 보안 검사
// 4. 실제 호출
1. 메서드 검색 (getMethod)
- 첫 호출 시 비용 큼
- 캐시되면 재호출은 빠름
2. 보안 검사
- 매 invoke마다 (setAccessible 안 했다면)
- 작은 비용이지만 누적
3. 매개변수 박싱
- primitive → Object[] 변환
- int → Integer
4. JIT 최적화 어려움
- invoke 호출은 동적이라 인라이닝 어려움
- 최근 JVM은 invokedynamic으로 일부 개선
일반 호출: 1 ns
Reflection (cold): 500 ns
Reflection (warm): 50 ns
Reflection + setAccessible: 20 ns
핵심:
// ❌ 매번 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 객체를 캐시하면 수십 배 빠름.
// 더 빠른 동적 호출
MethodHandle handle = MethodHandles.lookup()
.findVirtual(Shipment.class, "calculate", MethodType.methodType(BigDecimal.class, int.class));
BigDecimal result = (BigDecimal) handle.invokeExact(shipment, 100);
MethodHandle:
→ 고성능 동적 호출이 필요하면 MethodHandle.
→ 일반적으론 Reflection으로 충분.
Reflection은 정말 느린가?
답:
@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의 정수.
@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로 주입.
@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 기반.
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)
@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.
@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은 아니지만:
직접 사용: 거의 없음
간접 사용 (프레임워크 통해):
- Spring: 모든 DI/AOP
- JPA/Hibernate: 모든 매핑
- Jackson: 모든 JSON
- Spring Validation: 어노테이션 처리
- JUnit: 테스트 메서드 찾기
→ 박승제씨는 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);
}
// ❌ 모든 필드 setAccessible
for (Field f : obj.getClass().getDeclaredFields()) {
f.setAccessible(true);
// ...
}
문제:
→ 정말 필요할 때만.
// ❌ 사용자 입력을 그대로
@PostMapping("/dynamic")
public Object dynamic(@RequestParam String className) {
Class<?> clazz = Class.forName(className); // 위험!
return clazz.getDeclaredConstructor().newInstance();
}
문제:
해결:
// ❌
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 처리 중요.
List<Shipment> list = ...;
list.getClass(); // class java.util.ArrayList — 제네릭 정보 X
런타임에 제네릭 타입 정보 거의 사라짐 (Type Erasure).
해결:
ParameterizedType 사용public <T> List<T> findAll(Class<T> type) {
// ...
}
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에서 성공하지만 매우 위험
문제:
→ final 필드는 절대 변경 X.
Reflection은 캡슐화를 깰 수 있음:
- private 필드 접근
- 임의 메서드 호출
- 임의 객체 생성
위험:
- 악성 라이브러리가 시스템 정보 빼냄
- 신뢰할 수 없는 코드 실행
- 보안 우회
자바의 대응:
- SecurityManager (deprecated, 곧 제거)
- Java 9+ 모듈 시스템 (--add-opens)
- 단계적으로 Reflection 제한
# Reflection 호출 추적 (디버그 옵션)
java -Dsun.reflect.debugInflation=true App
# 어떤 클래스가 어떤 어노테이션 가지나
# IntelliJ → Run → "Show Annotations"
| 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 중 발생한 예외 래핑 |
1. Reflection = 런타임 자기 검사/조작
2. 4가지 능력
3. ILIC 실무
Phase 6의 다음 주제 (Iterator)는 짧게 다루고, Phase 7 (Buffer)도 응용 내용.
박승제씨의 1주차 학습에서 이미 본 부분들이 있어서, Phase 6/7은 통합/간략화 가능.
박승제씨가 원하는 방향:
1. Phase 6 Iterator 패턴 본격
2. Phase 7 Buffer (1주차 NIO와 통합)
3. 또는 2주차 종합 정리 + 다른 학습 진입
🚀 Phase 6 — Reflection & Iterator
✅ Unit 6.1 Reflection의 본질 ← 여기
⏭ (남은 Iterator 학습은 박승제씨 요청에 따라)
✅ 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
누적 24개 Unit
2주차 약 90% 완주