Reflecti의 어원
reflect은 반사하다, 비추다는 의미를 가지고 있다.
이를 객체지향에서의 관점에서 보면 예를 들어 강아지를 클래스 거울에 속에 비친 강아지를 JVM 메모리 영역의 정보라고 볼 수 있다.
즉 강아지를 직접 조작하는 것이 아니라 거울에 비친 강아지의 모습을 보고 클래스를 검사하고 조작하는 기술이라고 볼 수 있다.
reflect의 정의
런타임에 클래스와 인터페이스 등을 검사하고 조작할 수 있는 기능
런타임: 컴파일된 코드를 실행시키며 외부 링크와 운영체제 사용자와 상호작용하는 단계
리플랙션의 동작 원리
JAVA source(.java)코드를 Java Compiler가 컴파일하면. class파일인 (Java Byte Code)가 된다.
이를 Class Loader가 Runtime Data Areas에 올려주게 된다.
이중 Method Area에 클래스의 수준의 정보가 올라가게 된다.
즉 위의 예시에서 강아지를 Java Source(.java)로 볼 수 있고 거울에 반사된 강아지를 Method Area에 있다고 볼 수 있다.
reflection이란 Method Area에 있는 메타 정보를 가지고 런타임에 클래스를 검사하고 조작하는 기능이라고 볼 수 있다.
그럼 Method Area에 어떻게 접근할까?
Class를 사용하면 된다
Class는 일종의 Reflection을 할수있는 거울로써 메서드 영역의 클래스 및 인터페이스 정보 가져오는 전용 클래스라고 볼 수 있다.
Class는 실행중인 자바 어플리케이션의 클래스와 인터페이스의 정보를 가진 클래스이다.
Class 객체는 JVM에 의해 자동생성 되므로 public 생성자가 존재하지 않는다.
클래스를 만드는 방법은 아래와 같이 3가지가 있다.
//방법 1
final Class<Dog> dogClass1 = Dog.class;
//방법 2
final Dog obj = new Dog();
final Class<? extends Dog> dogClass2 = obj.getClass();
//방법 3
final Class<?> dogClass3 = Class.forName("~~.app.Dog"); //풀 패키지 정보
리플렉션으로 얻을 수 있는 정보
- 필드
- 메서드
- 생성자 (객체를 생성할 수 있다.)
- Enum
- Annotation
- 배열
- 부모 클래스와 인터페이스
- 등등...
클래스를 통해 사용할 수 있는 메소드는 아래와 같다.
참조 범위 | 접근 제어자 | |
getXXXs | 자신과 상위 클래스 | public |
getDeclaredXXXs | 자신 | 모두 |
사용 방법은 아래와 같다
class Dog {
private final String name = "뽀비";
}
public class Main {
public static void main(String[] args) throws Exception {
final Class<Dog> dogClass = Dog.class;
final Field nameField = dogClass.getDeclareFiled("name");
nameField.setAccessible(true); // private에 접근하려면 필요하다
final Dog dog = new Dog();
final String name = (String) nameFiled.get(dog);
System.out.println("dog name:" + name);
}
}
Reflection 활용
public interface DiscountPolicy {
void discount();
}
public class FixDiscount implements DiscountPolicy {
@Overrid
public void discount() {
System.out.println("고정 할인");
}
}
public class RateDiscount implements DiscountPolicy {
@Overrid
public void discount() {
System.out.println("비율 할인");
}
}
위 코드를 Runtime에서 할인 정책을 바꾸려면 아래와 같이 작성할 수 있다.
Reflection을 사용하면 구현체에 의존하지 않고 인터페이스에 의존하게 코드를 작성할 수 있다.
public class Main {
public static void main(String[] args) throws Exception {
final String strategyClassName = args[0]; //사용자가 선택한 할인 전략의 풀 패키지 이름
final Class<?> clazz = Class.forName(strategyClassName);
final Constructor<?> constructor = clazz.getDeclaredConstructor();
final DiscountPolicy discountPolicy = (DiscountPolicy) constructor.newInstance();
discountPolicy.discount();
}
}
리플렉션의 단점
1. 보안 취약점
2. 코드 복잡도 증가
3. 성능 저하
4. 최적화방해 (컴파일 시점이 아니라 런타임 시전에 클래스를 분석하기 때문에 JVM이 최적화 불가능)
5. 타입 안전성
6. 호환성
등에 단점이 있다.
리플렉션이 사용되는 곳
주로 프레임워크, 라이브러리에서 사용된다
프레임워크나 라이브러리는 사용자의 객체가 컴파일 시점엔 객체의 타입을 모른다.
- JPA
- Jackson
- Mockito
- JUnit
- ...
등 다양한 곳에서 사용된다.
기본생성자가 필요한 이유
프레임워크나 라이브러리에서는 기본생성자가 필요한 경우가 있다.
예로 JPA의 Entity이다
리플렉션으로 객체를 생성할 수 있고 생성자가 있지만 기본 생성자로 객체를 생성하고 필드를 주입하는 것이 가장 간단한 방법이기 때문이다.
프레임워크나 라이브러리는
- 어떤 생성자를 사용할지 고르기 어렵다.
- 생성자에 로직이 있는 경우 원하는 값을 필드에 넣어줄 수 없다.
- 파라미터들의 타입이 같은 경우 필드와 이름이 다르면 값을 알맞게 넣어주기 힘들다.
기본 생성자를 사용하면 위의 상황을 고려할 필요가 없다.
기본 생성자로 객체를 생성한 뒤 필드에 맞게 값을 넣어주면 된다.
어노테이션에도 리플렉션을 사용한다.
- 리플렉션을 통해 클래스나, 메서드, 파라미터 정보를 가져온다
- 리플렉션의 getAnnotation(s), GetDeclaredAnnotation(s) 등의 메서드를 통해 원하는 어노테이션이 붙어 있는지 확인한다.
- 어노테이션이 있다면 해당 로직을 수행한다.
리플렉션을 통해 DI를 만들 수 있다.
특정 타입의 클래스를 인자로 받으면 인스턴스를 생성해 리플렉션을 통해 필드를 순회하며 @Autowired가 붙은 필드가 있다면 해당 필드에 해당하는 인스턴스를 만들어 setter를 통해 주입을 한다.
'자바' 카테고리의 다른 글
For vs Stream (0) | 2024.06.23 |
---|---|
Record란 (0) | 2024.06.07 |