개요
Reflection은 런타임 환경에서 클래스, 생성자, 필드, 메서드, Annotation 등을 동적으로 조회하고 조작할 수 있게 도와주는 기술입니다. 이를 통해 컴파일 시점이 아닌 런타임 시점에 객체를 생성하거나 필드에 접근하고 메서드를 호출하는 것이 가능하며, Spring에서는 Dependency Injection, Bean 생성, Anntation 처리 등을 위해 Reflection을 적극적으로 활용합니다. 해당 글에서는 Reflection을 사용하는 이유와 주요 기능들을 예제와 함께 다뤄보도록 하겠습니다.
Reflection이란
앞서 말한 것처럼, Reflection은 런타임 환경에서 클래스, 생성자, 필드, 메서드, Annotation 등을 동적으로 조회하고 조작할 수 있게 도와주는 Java API 입니다. 일반적인 Java 코드는 컴파일 시점에 타입이 결정되지만, Reflection을 사용하면 런타임 시점에 클래스 정보를 분석하고 객체를 생성하거나 필드 및 메서드에 접근할 수 있습니다. 예를 들어, 일반적인 객체의 생성과 Reflection을 이용한 객체를 생성하는 방법은 아래와 같습니다.
User user = new User();
Class<?> clazz = Class.forName(me.jisung.user.entity.UserEntity);
Object user = clazz.getDeclaredConstructor().newInstance();
간단한 예제만 봤을때는 Reflection의 코드가 매우 복잡하기 때문에 일반적인 코드를 사용하면 되지 않을까 싶지만, Reflection을 사용한 코드는 매우 강력한 장점을 가지고 있습니다. 컴파일 시점이 아닌 런타임 시점에 객체를 동적으로 생성할 수 있다는 것과 클래스 이름을 문자열로 처리하는 등 애플리케이션 운영에 매우 강력한 확장성을 제공하며 Spring에서도 이를 적극적으로 활용하고 있습니다. Spring에서 Reflection을 활용하고 있는 기능들은 아래와 같습니다. 즉, Spring의 핵심 기능 대부분이 Reflection 기반으로 구현되어 있습니다.

Reflection을 사용하는 이유와 사용법
Spring Framework가 Reflection을 사용하는 가장 큰 이유는 동적처리를 가능하게 하기 위해서입니다. 일반적인 Java 코드에서는 아래와 같이 객체를 직접 생성합니다. 해당 방식은 컴파일 시점에 타입이 결정됩니다.
UserService userService = new UserService();
반면에, Spring에서는 객체를 new를 사용해 생성하지 않습니다. 아래와 같이 @Service 등의 어노테이션을 선언하면, 실행 시점에 자동으로 Bean에 등록됩니다. Classpath에서 클래스를 스캔하고 빈 등록 대상 어노테이션(@Service, @Repository 등)을 확인합니다. 이후 Reflection으로 객체를 생성, Bean Container에 저장하고 필요한 곳에 의존성을 주입니다. 즉, Spring은 컴파일 시점이 아닌 런타임 시점에 타입을 분석하고 객체를 제어 해야 하며, 이를 가능하게 하는 기술이 바로 Reflection 입니다.
@Service
public class UserService {
}
Reflection은 Class 객체를 기반으로 동작합니다. Class 객체를 가져오는 방법은 아래 3가지 방식이 있습니다. 해당 글에서는 예제를 위해 Class를 생성하는 방법을 설명하지만, Spring이 Bean을 생성할때는 이미 클래스 정보를 알고 있는 상태입니다. 그 이유는 Classpath 스캔을 통해 클래스 파일을 읽고 BeanDefinition을 만들면서 미리 Class 객체를 확보해두기 때문입니다.
// 방법 1: 클래스 기반 - 컴파일 시점에 클래스가 존재할 때 사용합니다.
Class<User> clazz = User.class;
// 방법 2: 객체 기반 - 이미 생성된 객체가 있을 때 사용합니다.
User user = new User();
Class<?> clazz = user.getClass();
// 방법 3: 문자열 기반 (동적 로딩) - 문자열 기반으로 클래스를 로드합니다.
Class<?> clazz = Class.forName("me.jisung.user.entity.UserEntity");
Class 객체를 사용해 new 키워드 없이 객체를 생성하는 방법은 아래와 같습니다. Reflection은 런타임 환경에서 동작하기 때문에 예외처리 코드가 필수입니다.
try {
Class<?> clazz = Class.forName("cohttp://m.example.User");
Object instance = clazz.getDeclaredConstructor().newInstance();
} catch (ClassNotFoundException e) {
throw new RuntimeException("클래스를 찾을 수 없습니다.", e);
} catch (NoSuchMethodException e) {
throw new RuntimeException("생성자를 찾을 수 없습니다.", e);
} catch (Exception e) {
throw new RuntimeException("객체 생성 실패", e);
}
Reflection을 사용한 필드 접근 방식입니다. Reflection을 사용하면 setAccessible(true)를 이용해 private 필드에도 접근할 수 있으며, Spring에서 DI를 할때에도 아래와 같은 방식으로 의존성을 주입합니다.
try {
Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
field.set(instance, "Jisung");
} catch (NoSuchFieldException e) {
throw new RuntimeException("필드를 찾을 수 없습니다.", e);
} catch (IllegalAccessException e) {
throw new RuntimeException("필드 접근 실패", e);
}
Reflection을 사용한 메서드 호출 방법입니다. 아래와 같이 동적으로 메서드를 호출할 수 있습니다.
try {
Method method = clazz.getDeclaredMethod("setName", String.class);
method.invoke(instance, "Jisung");
} catch (NoSuchMethodException e) {
throw new RuntimeException("메서드를 찾을 수 없습니다.", e);
} catch (IllegalAccessException e) {
throw new RuntimeException("메서드 접근 실패", e);
} catch (InvocationTargetException e) {
throw new RuntimeException("메서드 실행 중 오류 발생", e);
}
Reflection을 사용한 어노테이션 처리 예제입니다. Spring도 아래 방식으로 @Autowired, @Service 등 어노테이션을 처리합니다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyAnnotation {
String value();
}
@MyAnnotation("example")
private String name;
Field field = clazz.getDeclaredField("name");
MyAnnotation annotation = field.getAnnotation(MyAnnotation.class);
String value = annotation.value();
Reflection 사용의 장단점
Reflection 사용 시 장점
- 컴파일 시점이 아닌 런타임 시점에 타입을 분석하고 객체를 조작할 수 있습니다.
- setAccessible()을 사용해 클래스 내부 private 필드에 접근할 수 있습니다.
- 제어권을 프레임워크로 이동시킬 수 있습니다. 제어의 역전을 구현하는 핵심 기술입니다.
- 런타임 환경에서 Annotation 정보를 동적으로 읽을 수 있습니다.
Reflection 사용 시 단점
- Reflection은 일반적인 메서드 호출보다 성능이 느립니다.
- 캄파일 시점에 타입 안정성을 보장하지 않습니다.
- private 필드 접근이 가능하기 때문에 객체 지향 설계 원칙인 캡슐화를 위반할 수 있습니다.
- 런타임 시 오류가 발생할 수 있기 때문에 디버깅이 어려워질 수 있습니다.
결론
Reflection은 런타임 환경에서 동적 처리를 가능하게 하여 Spring, Hibernate, Jackson과 같은 현대 Java 프레임워크의 기반이 되는 핵심 기술입니다. 하지만 성능, 타입 안정성, 캠슐화 측면에서도 단점이 존재하기 때문에 일반 애플리케이션 코드에서는 사용을 최소화 하는 것이 바람직하다고 생각합니다. 실무에서 Reflection을 자주 사용할 일은 많지 않지만, Spring의 내부 동작 원리를 이해하기 위해 반드시 알아야 할 중요한 개념입니다.