1편에서는 자바 리플렉션의 기본 개념과 Class 객체를 얻는 방법, 필드/메서드/생성자를 조작하는 기본 사용법을 알아보았습니다.
이번 2편에서는 리플렉션이 실제로 어떻게 활용되는지, 사용 시 주의해야 할 점은 무엇인지 자세히 살펴보겠습니다.
Spring은 리플렉션을 사용하여 런타임에 객체를 생성하고 의존성을 주입합니다.
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
}
Spring의 내부 동작 방식
*// Spring이 @Autowired를 처리하는 방식 (단순화)*
public void injectDependencies(Object target) throws Exception {
Class<?> clazz = target.getClass();
*// @Autowired가 붙은 필드 찾기*
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Autowired.class)) {
field.setAccessible(true);
*// 필요한 빈을 찾아서 주입*
Object dependency = findBean(field.getType());
field.set(target, dependency);
}
}
}
JPA는 리플렉션을 통해 데이터베이스의 결과를 엔티티 객체로 변환합니다.
@Entity
public class User {
@Id
private Long id;
@Column(name = "user_name")
private String name;
private String email;
}
JPA의 내부 동작 방식
*// JPA가 ResultSet을 엔티티로 변환하는 방식 (단순화)*
public <T> T mapResultSetToEntity(ResultSet rs, Class<T> entityClass) throws Exception {
*// 엔티티 인스턴스 생성*
T entity = entityClass.getDeclaredConstructor().newInstance();
*// 모든 필드 순회*
for (Field field : entityClass.getDeclaredFields()) {
field.setAccessible(true);
*// @Column 어노테이션에서 컬럼명 가져오기*
String columnName = field.getName();
if (field.isAnnotationPresent(Column.class)) {
Column column = field.getAnnotation(Column.class);
columnName = column.name();
}
*// ResultSet에서 값을 가져와 필드에 설정*
Object value = rs.getObject(columnName);
field.set(entity, value);
}
return entity;
}
JUnit은 리플렉션을 사용하여 @Test 어노테이션이 붙은 메서드를 찾아 실행합니다.
public class UserServiceTest {
@Test
public void testCreateUser() {
*// 테스트 코드*
}
@Test
public void testFindUser() {
*// 테스트 코드*
}
}
JUnit의 내부 동작 방식
*// JUnit이 테스트를 실행하는 방식 (단순화)*
public void runTests(Class<?> testClass) throws Exception {
*// 테스트 클래스 인스턴스 생성*
Object testInstance = testClass.getDeclaredConstructor().newInstance();
*// @Test 어노테이션이 붙은 메서드 찾기*
for (Method method : testClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(Test.class)) {
try {
*// 테스트 메서드 실행*
method.invoke(testInstance);
System.out.println(method.getName() + " - PASSED");
} catch (Exception e) {
System.out.println(method.getName() + " - FAILED");
}
}
}
}
JSON 라이브러리는 리플렉션을 사용하여 객체를 JSON으로, JSON을 객체로 변환합니다.
public class User {
private String name;
private int age;
private String email;
}
JSON 직렬화/역직렬화 예시
// JSON을 객체로 변환하는 방식 (단순화)
public <T> T fromJson(String json, CLass<T> clazz) throws Exception {
// JSON 파싱
Map<String, Object> jsonMap = parseJson(json);
// 객체 생성
T object = clazz.getDeclaredCounstructor().newInstance();
// 필드에 값 설정
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
String fieldName = field.getName();
if (jsonMap.containsKey(fieldName)) {
Object value = jsonMap.get(fieldName);
field.set(object, value);
}
}
return object;
}
// 객체를 JSON으로 변환하는 방식 (단순화)
public String toJson(Object obj) throws Exception {
StringBuilder json = new StringBuilder("{");
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
field.setAccessible(true);
String fieldName = field.getName();
Object value = field.get(obj);
json.append("\"").append(fieldName).append("\":")
.append("\"").append(value).append("\"");
if (i < fields.length - 1) {
json.append(",");
}
}
json.append("}");
return json.toString();
}
리플렉션은 일반적인 메서드 호출보다 훨씬 느립니다.
// 성능 비교 예제
public class PerformanceTest {
public static void main(String[] args) throws Exception {
Example example = new Example();
// 일반 메서드 호출
long start1 = System.nanoTime();
for (int i = 0; i < 1000000; i++) {
example.doSomething();
}
long end1 = System.nanoTime();
System.out.println("일반 호출: " + (end1 - start1) + "ns");
*// 리플렉션을 통한 호출*
Method method = Example.class.getMethod("doSomething");
long start2 = System.nanoTime();
for (int i = 0; i < 1000000; i++) {
method.invoke(example);
}
long end2 = System.nanoTime();
System.out.println("리플렉션 호출: " + (end2 - start2) + "ns");
}
}
class Example {
public void doSomething() {
*// 작업 수행*
}
}
결과 예시
일반 호출: 5000230ns 리플렉션 호출: 250000012ns (약 50배 느림)
리플렉션은 런타임에 동작하므로 컴파일 시점에 오류를 찾을 수 없습니다.
public class ReflectionError {
public static void main(String[] args) {
try {
Class<?> clazz = Class.forName("com.example.User");
*// 존재하지 않는 필드명 - 컴파일 시점에는 오류가 없음*
Field field = clazz.getDeclaredField("userNaem"); *// 오타!*
} catch (ClassNotFoundException e) {
System.out.println("클래스를 찾을 수 없습니다.");
} catch (NoSuchFieldException e) {
System.out.println("필드를 찾을 수 없습니다."); *// 런타임에 발견*
}
}
}
리플렉션은 private 멤버에 접근할 수 있어 캡슐화를 깨뜨립니다.
public class EncapsulationViolation {
public static void main(String[] args) throws Exception {
BankAccount account = new BankAccount(1000);
// private 필드에 직접 접근하여 잔액 변조
Field balanceField = BankAccount.class.getDeclaredField("balance");
balanceField.setAccessible(true);
balanceField.set(account, 1000000);
System.out.println(account.getBalance()); *// 1000000*
}
}
class BankAccount {
private int balance;
public BankAccount(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void deposit(int amount) {
if (amount > 0) {
balance += amount;
}
}
}
Java 9 이상에서는 모듈 시스템으로 인해 리플렉션 사용이 제한됩니다.
*// Java 9+ 에서 경고 발생*
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.example.MyClass
WARNING: Please consider reporting this to the maintainers
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
해결 방법
*# JVM 옵션으로 특정 패키지 열기*
java --add-opens java.base/java.lang=ALL-UNNAMED MyApp
리플렉션 객체를 재사용하여 성능을 개선합니다.
public class ReflectionCache {
// Method 객체를 캐싱
private static final Map<String, Method> methodCache = new HashMap<>();
public static Method getCachedMethod(Class<?> clazz, String methodName)
throws NoSuchMethodException {
String key = clazz.getName() + "#" + methodName;
// 캐시에서 먼저 확인
Method method = methodCache.get(key);
if (method == null) {
method = clazz.getMethod(methodName);
method.setAccessible(true);
methodCache.put(key, method);
}
return method;
}
public static void main(String[] args) throws Exception {
Example example = new Example();
// 캐싱된 Method 재사용
Method method = getCachedMethod(Example.class, "doSomething");
for (int i = 0; i < 1000000; i++) {
method.invoke(exaplme);
}
}
}
리플렉션 작업은 다양한 예외를 발생시킬 수 있습니다.
public class SafeReflection {
public static Object safeInvoke(Object target, String methodName, Object... args) {
try {
Class<?> clazz = target.getClass();
*// 파라미터 타입 배열 생성*
Class<?>[] paramTypes = new Class[args.length];
for (int i = 0; i < args.length; i++) {
paramTypes[i] = args[i].getClass();
}
*// 메서드 찾기 및 실행*
Method method = clazz.getMethod(methodName, paramTypes);
return method.invoke(target, args);
} catch (NoSuchMethodException e) {
System.err.println("메서드를 찾을 수 없습니다: " + methodName);
} catch (IllegalAccessException e) {
System.err.println("메서드에 접근할 수 없습니다: " + methodName);
} catch (InvocationTargetException e) {
System.err.println("메서드 실행 중 오류 발생: " + e.getCause());
}
return null;
}
}
리플렉션은 최후의 수단으로 사용해야 합니다.
*// ❌ 나쁜 예: 불필요한 리플렉션 사용*
public void badExample() throws Exception {
Class<?> clazz = String.class;
Method method = clazz.getMethod("length");
String str = "Hello";
int length = (int) method.invoke(str);
}
*// ✅ 좋은 예: 일반적인 방법 사용*
public void goodExample() {
String str = "Hello";
int length = str.length();
}
가능하면 public API를 통해 접근합니다.
public class AccessibleExample {
*// ❌ 나쁜 예*
public void badExample(User user) throws Exception {
Field field = User.class.getDeclaredField("name");
field.setAccessible(true);
String name = (String) field.get(user);
}
*// ✅ 좋은 예*
public void goodExample(User user) {
String name = user.getName(); *// public getter 사용*
}
}
핵심 내용 요약