[자바/Java] Reflection의 개념 및 사용법 (2)

dongbrown·2025년 8월 19일

Java

목록 보기
6/6

🔄 이전 글 요약

1편에서는 자바 리플렉션의 기본 개념과 Class 객체를 얻는 방법, 필드/메서드/생성자를 조작하는 기본 사용법을 알아보았습니다.

이번 2편에서는 리플렉션이 실제로 어떻게 활용되는지, 사용 시 주의해야 할 점은 무엇인지 자세히 살펴보겠습니다.


🎯 리플렉션의 실전 활용 사례

1️⃣ Spring Framework의 의존성 주입(DI)

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);
        }
    }
}

2️⃣ JPA/Hibernate의 엔티티 매핑

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;
}

3️⃣ JUnit의 테스트 실행

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");
            }
        }
    }
}

4️⃣ JSON 라이브러리 (Jackson, Gson)

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();
}

⚠️ 리플렉션 사용 시 주의사항

1️⃣ 성능 오버헤드

리플렉션은 일반적인 메서드 호출보다 훨씬 느립니다.

// 성능 비교 예제
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배 느림)

2️⃣ 컴파일 타임 안정성 상실

리플렉션은 런타임에 동작하므로 컴파일 시점에 오류를 찾을 수 없습니다.

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("필드를 찾을 수 없습니다."); *// 런타임에 발견*
        }
    }
}

3️⃣ 캡슐화 위반

리플렉션은 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;
        }
    }
}

4️⃣ 보안 문제

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

💡 리플렉션 사용 Best Practice

1️⃣ 캐싱 활용

리플렉션 객체를 재사용하여 성능을 개선합니다.

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);
					}
			}
}

2️⃣ 예외 처리를 철저히

리플렉션 작업은 다양한 예외를 발생시킬 수 있습니다.

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;
    }
}

3️⃣ 꼭 필요한 경우에만 사용

리플렉션은 최후의 수단으로 사용해야 합니다.

*// ❌ 나쁜 예: 불필요한 리플렉션 사용*
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();
}

4️⃣ setAccessible() 사용 최소화

가능하면 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 사용*
    }
}

🎯 정리

핵심 내용 요약

  • 리플렉션은 Spring, JPA, JUnit 등 주요 프레임워크의 핵심 기술
  • 성능 오버헤드, 타입 안정성 상실, 캡슐화 위반 등의 단점 존재
  • 캐싱, 철저한 예외 처리, 최소한의 사용이 중요
  • 꼭 필요한 경우에만 신중하게 사용해야 함

0개의 댓글