개요
자바 리플렉션이 무엇인가에 대해서 공부하고 공부한 내용을 토대로 작성했다!
프레임워크와 라이브러리들을 사용하면서 어노테이션과 여러 기능을 사실 자바 코드로 어떻게 하는지 이해할 수 없었는데 이해가 된 것 같다.
java 애플리케이션 의문점들..
스프링은 어떻게 런타임 시점에 빈을 주입할 수 있을까?
많은 프레임워크나 라이브러리들은 왜 기본 생성자를 필요로 할까?
JUnit은 @Test 언노테이션이 붙은 메서드들을 어떻게 실행시킬까?
정답은 바로 리플렉션!
reflection이란?
리플렉션의 뜻은 거울에 비친 상, 모습
이라는 뜻이다.
자바에서는 거울, 물에 비친 상은 실제 클래스의 정보가 비쳐진 런타임 시점의 클래스 데이터를 뜻한다!
자바 코드 동작 과정
자바 코드인 .java 파일은 자바 컴파일러에 의하여 JVM이 이해할 수 있는 .class 파일로 컴파일된다.
JVM은 이 .class 파일을 OS에 따라서 실행시키며 리플렉션은 Runtime Data Area에 있는 데이터를 사용한다.
Runtime Data Area는 위의 그림처럼 생겼다.
여기서 클래스에 대한 정보는 Heap에 포함되어 있다.
Heap의 Permanent Generation에 있는 클래스 정보를 리플렉션은 런타임 시점에 불러와서 사용한다.
자바 리플렉션 패키지
자바의 기본 패키지들 중 하나인 java.lang.* 패키지에 포함되어 있다.
런타임 시점에서 클래스에 동작을 검사하거나 필드, 메서드, 수정자 등의 정보들을 조작할 수 있도록 지원한다.
자바 Class
클래스
리플렉션의 핵심 클래스는 Class
이다.
- Class 클래스는 실행중인 자바 어플리케이션의 클래스와 인터페이스 정보를 가진 클래스
- 클래스에 붙은 어노테이션, 생성자, 필드, 메서드, 부모 클래스, 인터페이스 등의 정보를 조회할 수 있다.
- Class 클래스는 생성자(public)를 통해 객체화하는 것이 아닌 JVM에 의해 객체화된다.
Class
가져오기
- {클래스 타입}.class
Class<?> clazz = Dog.class;
- {인스턴스}.getClass()
Dog dog = new Dog("kimchi");
Class<?> clazz = dog.getClass();
- Class.forName(”{전체 도메인 네임}”)
Class<?> clazz = Class.forName("org.example.Dog");
Class의 메서드 사용 시 주의점
getMethods
상위 클래스와 상위 인터페이스에서 상속한 메서드를 포함
하여 public
인 메서드들을 모두 가져온다.
getDeclaredMethods
접근 제어자와 관계 없이 상속한 메서드를 제외
하고 직접 클래스에서 선언
한 메서드들을 모두 가져온다.
→ getXXX 와 getDeclaredXXX를 잘 구분해서 사용하자!
리플렉션 기능 1 : 생성자를 통한 객체 생성
생성자의 파라미터로 구분하여 클래스에 선언된 생성자를 가져올 수 있다.
Class<?> clazz = Class.forName("org.example.Dog");
Constructor<?> constructor1 = clazz.getDeclaredConstructor();
Constructor<?> constructor2 = clazz.getDeclaredConstructor(String.class);
Constructor<?> constructor3 = clazz.getDeclaredConstructor(String.class, int.class);
Object dog1 = constructor1.newInstance();
- 만약 생성자의 접근제어자가 private이라면 접근이 불가하기 때문에 예외가 발생한다.
→ 하지만 리플렉션을 통해 접근할 수 있다.
접근 제어자가 public이 아닌 경우 setAccessible 메서드를 이용하면 접근할 수 있다.
constructor1.setAccessible(true);
Object dog1 = constructor1.newInstance();
Object dog2 = constructor2.newInstance("호두");
Object dog3 = constructor3.newInstance("호두", 5);
리플렉션 기능 2 : 필드 정보 조회, 값 변경
필드의 접근제어자, 타입, 네임, 값 등의 정보를 조회할 수 있다.
Object dog = constructor.newInstance("호두", 5);
Field[] fields = clazz.getDeclaredFields();
for(Field field : fields){
field.setAccessible(true);
System.out.println(field);
System.out.println("value : " + field.get(dog));
}
private 필드의 값도 변경할 수 있다.
Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
System.out.println("before : " + field.get(dog));
field.set(dog, "gugu")
System.out.println("after : " + field.get(dog));
리플렉션 기능 3 : 메서드 관련 기능
메서드의 접근제어자, 리턴 타입, 네임, 파라미터 타입 등의 정보를 가져올 수 있다.
Method[] methods = clazz.getDeclaredMethods();
for(Method method : methods){
method.setAccessible(true);
System.out.println(method);
}
마찬가지로 private 메서드도 호출할 수 있다.
Method method = clazz.getDeclaredMethod("speak", String.class, int.class);
method.setAccessible(true);
method.invoke(dog, "hey", 5);
리플렉션을 왜 사용할까?
주로 프레임워크와 라이브러리에서 사용된다.
사용자가 사용하는 객체의 타입을 컴파일시점에는 알 수 없다.
→ 이러한 문제를 동적으로 해결하기 위해서 사용된다.
리플렉션이 사용되는 곳
- JPA
- Jackson
- Jackson 은 자바 객체와 JSON 데이터 간의 변환을 쉽게 처리할 수 있는 강력한 라이브러리로, JSON 직렬화(serialization) 및 역직렬화(deserialization)를 지원한다.
- Mockito
- JUnit
intellij의 자동완성 기능도 리플렉션을 사용한 기능이다.!!!!
많은 프레임워크나 라이브러리에서는 객체에 기본 생성자
가 왜 필요할까요?
- JPA 엔티티
- RequestDTO
- ResponseDTO
→ 기본 생성자가 필요한 이유는 리플렉션이다!
리플렉션으로 객체를 생성할 수 있는데 왜 또 기본 생성자가 필요할까?
기본 생성자로 객체를 생성하고, 필드를 통해 값을 넣어주는 것이
가장 간단한 방법
이기 때문이다.
- 만약 기본 생성자가 없다면 여러 생성자가 있을 때 어떤 생성자를 사용할지 고르기가 어렵다.
- 생성자에 로직이 있는 경우 원하는 값을 바로 넣어줄 수 없다.
- 파라미터의 타입이 같은 경우 필드와 이름이 다르면 값을 알맞게 넣어주기 힘들다.
→ 기본 생성자를 사용할 경우 이 모든 경우의 수들을 고려하지 않고 생성자로 객체를 생성한 후 필드 이름에 맞춰 알맞은 값을 넣어주면 끝난다.
어노테이션
어노테이션은 그냥 주석인데 어떻게 동작하는 것일까?
→ 어노테이션 동작 원리도 리플렉션이다!~!!
- 리플렉션을 통해 클래스나 메서드, 파라미터 정보를 가져온다.
- 리플렉션의 getAnnotation(s), getDeclaredAnnotation(s) 등의 메서드를 통해 원하는 어노테이션이 붙어 있는지 확인한다.
- 원하는 어노테이션이 붙어있다면 원하는 로직을 수행한다.
→ 리플렉션은 생각보다 우리 가까이에 있다.
간단한 DI 프레임워크 만들기
- Autowired 어노테이션 만들기
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired{
}
- ApplicationContext
public class ApplicationContext{
public static <T> T getInstance(Class<T> clazz) throws Exception{
T instance = createInstance(clazz);
Field[] fields = clazz.getDeclaredFileds();
for(Field field : fields){
if(field.getAnnotation(Autowiredl.class) != null){
Object fieldInstance = createInstance(field.getType());
field.setAccessible(true);
field.set(instance, fieldInstance);
}
}
}
private static <T> T createInstnce(Class<T> clazz) throws Exception{
return clazz.getConstructor(null).newInstance();
}
}
- 실행
public class Application{
public static void main(String[] args){
ApplicationContext applicationContext = new applicationcontext();
OrderService orderService = applicationContext.getInstance(OrderService.class);
}
}
리플렉션의 단점
단점 1 : 일반 메서드 호출보다 성능이 훨씬 떨어진다.
Reflection API는 컴파일 시점이 아니라
런타임 시점
에서 클래스를 분석한다.
→ JVM을 최적화할 수 없기 때문에 성능 저하가 발생한다.
일반 메서드보다 24배 차이가 난 결과가 있다.
- 내가 테스트했을 때는 차이가 없었다.
단점 2 : 컴파일 시점에서 타입 체크 기능을 사용할 수 없다.
리플렉션은
런타임 시점
에 클래스 정보를 알게 되기 때문에 컴파일러가 에러를 발생시키지 않습니다!!
단점 3 : 코드가 많이 길어진다.
단점 4 : 내부를 노출해서 추상화를 파괴한다.
접근할 수 없는 메서드나 필드에 접근할 수 있고 모든 클래스의 정보를 알게되기 때문에 추상화와 불변성도 지킬 수 없게 된다.
따라서 리플렉션은 아주 제한된 형태로만 사용해야 한다.
→ 클래스의 정보를 컴파일 시점에 알아야하는 특수한 경우가 아니라면 리플렉션의 사용을 지양해야 한다.
참고자료
'개발 관련' 카테고리의 다른 글
[CS] 프로세스와 스레드 (1) | 2024.08.28 |
---|---|
[CS] 멀티태스킹과 멀티프로세싱이 뭐지? (0) | 2024.08.27 |
[Spring Boot] 스프링 부트 HTTPS 적용기 (0) | 2024.01.26 |