Reflection(리플렉션)이란?
: 클래스가 제공하는 다양한 정보를 동적으로 분석하고 사용하는 기능
=> 리플렉션을 통해 프로그램 실행 중에 클래스, 메서드, 필드 등에 대한 정보를 얻거나, 새로운 객체를 생성하고 메서드를 호출하며, 필드의 값을 읽고 쓸 수 있다
리플렉션을 통해 얻을 수 있는 정보
- 클래스의 메타데이터(metadata); 클래스 이름, 접근 제어자, 부모 클래스, 구현된 인터페이스 등
- 필드 정보: 필드의 이름, 타입, 접근 제어자를 확인하고, 해당 필드의 값을 읽거나 수정 가능
- 메서드 정보: 메서드 이름, 반환 타입, 매개변수 정보를 확인하고, 실행 중에 동적으로 메서드를 호출 가능
- 생성자 정보: 생성자의 매개변수 타입과 개수를 확인하고, 동적으로 객체 생성 가능
클래스 메타데이터
클래스 메타데이터 조회 방법 3가지
- 클래스에서 찾기: 클래스명에
.class
사용- ex)
Class<A> aClass = A.class;
- ex)
- 인스턴스에서 찾기: 인스턴스에서
.getClass()
호출A a = new A(); Class<? extends A> aClass = a.getClass();
- 문자로 찾기: 패키지명이 포함된 클래스명을 통해
Class.forName(className)
을 호출String className = "reflection.A"; Class<?> aClass = Class.forName(className);
클래스 메타데이터로 얻을 수 있는 정보
getName()
: 패키지명 포함하는 클래스 이름getSimpleName()
: 클래스 이름getPackage()
: 패키지 정보getSuperclass()
: 부모 클래스getInterfaces()
: 구현한 인터페이스 리스트isInterface()
: 인터페이스 여부isEnum()
: ENUM 여부isAnnotation()
: 애노테이션 여부getAnnotations(Class<Annotation>)
: 해당 애노테이션 정보getModifiers()
: 수정자가 조합된 숫자Modifier.isPublic(int modifiers)
: 수정자가 public인지 여부Modifier.toString(int modifiers)
: 수정자를 string으로
메서드 메타데이터
메서드 메타데이터 조회 방법 2가지
클래스 메타데이터를 통해 클래스가 제공하는 메서드의 정보를 조회할 수 있다.
클래스메타데이터.getMethods()
: 해당 클래스와 상위 클래스에서 상속된 모든 public 메서드 반환클래스메타데이터.getDeclaredMethods()
: 접근 제어자에 관계없이 해당 클래스에서 선언된 모든 메서드 반환, 상속된 메서드는 포함 X
동적 메서드 호출
Method 객체(메서드 메타데이터)를 사용해서 메서드를 직접 호출할 수도 있다.
String methodName = "hello";
Method methodMetadata = helloClass.getMethod(methodName, String.class); // 메서드 이름과 사용하는 매개변수의 타입을 전달하여 원하는 메서드 찾기
Object returnValue = methodMetadata.invoke(helloInstance, "hi"); // 메서드에 실행할 인스턴스와 인자를 전달하여 해당 인스턴스에 있는 메서드 실행
=> methodName은 변수이므로 예를 들어 사용자 콘솔 입력을 통해서 얼마든지 호출할 methodName을 변경할 수 있다 => 코드 변경 없이 런타임에 동적 호출 가능
필드 탐색과 값 변경
필드 데이터 조회 방법 2가지
클래스 메타데이터를 통해 클래스에 존재하는 필드의 정보를 조회할 수 있다.
클래스메타데이터.fields()
: 해당 클래스와 상위 클래스에서 상속된 모든 public 필드를 반환클래스메타데이터.declaredFields()
: 접근 제어자에 관계없이 해당 클래스에서 선언된 모든 필드를 반환하며, 상속된 필
드는 포함하지 않음
필드값 변경
User user = new User("id1", "userA", 20);
System.out.println("기존 이름 = " + user.getName());
Class<? extends User> aClass = user.getClass();
Field nameField = aClass.getDeclaredField("name");
// private 필드에 접근 허용, private 메서드도 이렇게 호출 가능
nameField.setAccessible(true);
nameField.set(user, "userB");
System.out.println("변경된 이름 = " + user.getName());
setAccessible(true)
를 통해 private 메서드 혹은 필드에 접근할 수 있다.
생성자 탐색과 객체 생성
생성자 탐색 조회 방법 2가지
클래스 메타데이터를 통해 클래스에 존재하는 생성자를 탐색하고, 또 탐색한 생성자를 사용해서 객체를 생성할 수 있다.
클래스메타데이터.getConstructors()
: 해당 클래스와 상위 클래스에서 상속된 모든 public 생성자를 반환클래스메타데이터.getDeclaredConstructors()
: 접근 제어자에 관계없이 해당 클래스에서 선언된 모든 생성자를 반환하며, 상속된 생성자는 포함하지 않음
동적 객체 생성
Class<?> aClass = Class.forName("reflection.data.BasicData");
Constructor<?> constructor = aClass.getDeclaredConstructor(String.class);
constructor.setAccessible(true); // private 생성자 접근 가능
Object instance = constructor.newInstance("hello"); // newInstance(인자)를 통해 인스턴스를 생성할 수 있다.
System.out.println("instance = " + instance);
Method method1 = aClass.getDeclaredMethod("call");
method1.invoke(instance);
리플렉션과 주의사항
리플렉션을 활용하면 private
접근 제어자에도 직접 접근해서 값을 변경할 수 있다. 하지만 이는 객체 지향 프로그래밍의 원칙을 위반하는 행위로 간주될 수 있다. private
접근 제어자는 클래스 내부에서만 데이터를 보호하고, 외부에서의 직접적인 접근을 방지하기 위해 사용된다. 리플렉션을 통해 이러한 접근 제한을 무시하는 것은 캡슐화 및 유지보수성에 악영향을 미칠 수 있다. 예를 들어, 클래스의 내부 구조나 구현 세부 사항이 변경될 경우 리플렉션을 사용한 코드는 쉽게 깨질 수 있으며, 이는 예상치 못한 버그를 초래할 수 있다.
따라서 리플렉션을 사용할 때는 반드시 신중하게 접근해야 하며, 가능한 경우 접근 메서드(예: getter, setter)를 사용하는 것이 바람직하다. 리플렉션은 주로 테스트나 라이브러리 개발 같은 특별한 상황에서 유용하게 사용되지만, 일반적인 애플리케이션 코드에서는 권장되지 않는다. 이를 무분별하게 사용하면 코드의 가독성과 안전성을 크게 저하시킬 수있다.
리플렉션 사용 예제(스프링 DI와 빈등록)
리플렉션을 활용하면 정적인 기존 코드로 해결하기 어려운 공통 문제를 손쉽게 처리할 수 있다
대표적으로 스프링 프레임워크의 DI와 빈 자동 등록은 이러한 리플렉션 기법과 애노테이션을 이용하는데, 애노테이션에 대한 학습이 되어 있다고 가정하고 간단히 설명하면 다음과 같다.
스프링 DI
- 스프링 컨테이너는 자바 리플렉션을 통해 클래스 정보를 스캔하고, 애노테이션이 붙은 클래스와 필드를 탐지한다.
- @Autowired가 붙은 필드나 생성자가 있으면, 스프링은 그 의존성을 찾아 해당 객체를 주입한다.
- 이 과정에서 스프링은 리플렉션을 사용하여 객체를 동적으로 생성(싱글톤)하고, 필드에 값을 설정하거나 생성자를 호출하여 의존성을 해결한다.
예시 코드
@Component
public class MyService {
private final MyRepository myRepository;
@Autowired
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
}
public class ReflectionExample {
public static void main(String[] args) {
try {
// 1. 클래스 정보 획득
Class<?> clazz = Class.forName("com.example.MyClass");
// 2. 기본 생성자 가져오기
Constructor<?> constructor = clazz.getConstructor();
// 3. 객체 생성
Object instance = constructor.newInstance();
System.out.println("Created instance: " + instance);
} catch (Exception e) {
e.printStackTrace();
}
}
}
자동 빈 등록
- 스프링은 애플리케이션이 실행될 때,
@ComponentScan
애노테이션이 붙은 패키지를 기준으로 클래스패스를 스캔 - 스프링은
@Component
애노테이션이 붙어 있는 클래스를 리플렉션을 통해 탐지한 후, 해당 클래스를 스프링 컨테이너에 빈으로 등록
예시 코드Class<?> clazz = MyService.class; if (clazz.isAnnotationPresent(Component.class)) { // 클래스에 @Component 애노테이션이 붙어 있으면 빈으로 등록 가능 System.out.println(clazz.getName() + " is annotated with @Component"); }
'Java' 카테고리의 다른 글
[Java] 자바 기본 개념 정리 (기본 문법 제외) (2) | 2025.01.02 |
---|---|
[Java] Garbage Collection (3) | 2025.01.02 |
[Java] JVM / JRE / JDK (3) | 2025.01.01 |
[Java] Annotation (0) | 2024.10.10 |
[Java] I/O (0) | 2024.10.10 |