공부한 내용 정리한 포스팅입니다.
틀린내용 피드백 언제든 부탁드립니다.
리플렉션 API?
요약하자면
1. 실행중인 자바 프로그램이 자체적으로 검사하거나 프로그램 내부의 속성을 조작할 수 있기 해주는 API
2. 리플렉션을 사용하면 동적으로 로드되는 Class의 속성을 가져와서 사용한다.
3. 다른 언어에 없는 기능이다.
Class Class
1. Class의 인스턴스는 실행중인 Java 애플리케이션의 클래스 및 인터페이스를 나타낸다.
2. 생성자는 없지만 클래스가 로드되고 define Class Loade의 메소드 호출에 따라 JVM에 의해 자동으로 생성된다.
런타임에서 활용할수 있는 방법은 세가지다.
1. 클래스.class
2. Class.forname(className, true, currentLoader)
패키지를 포함한 클래스 또는 인터페이스와 찾아올 클래스의 정의 클래스 로더를 통해 가져온다.
3. 생성된 인스턴스.getClass()
클래스 인스턴스 활용
public class Book {
private String a = "a";
private static String b = "b";
private static final String c = "c";
public static String g= "g";
public String d = "d";
protected String e = "e";
public String hello(){
return "hello";
}
public Book() {
}
public Book(String a) {
this.a = a;
}
public Book(String a, String d) {
this.a = a;
this.d = d;
}
}
public class Main {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException {
Class<Book> bookClass = Book.class; // .class
Book book1 = new Book();
Class<? extends Book> bookClass2 = book1.getClass(); // instance.getClass
Class<?> bookClass3 = Class.forName("org.example.Book"); // Class.forName
}
}
Class.forName으로 이용할 때엔 문자열로 해당 위치의 클래스를 가져오기 때문에 없을 경우를 위해 Exception 처리가 필요하다.
이렇게 가져온 클래스 인스턴스를 이용해 필드(목록), 메서드(목록), 상위클래스, 인터페이스(목록), 생성자, 어노테이션등에 접근할 수 있다.
[https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html](Java Class Class Referecne Doc)
Filed, Method
Declared
클래스타입의 인스턴스에 접근할 때 Declared를 붙이면 해당 클래스에 접근제한자에 관계없이 해당 클래스에 선언된 것 모두를 가져오고 Declared를 붙이지 않는다면 public한 것들만 가져온다.
public class Main {
public static void main(String[] args) {
Class<Book> bookClass = Book.class; // .class
Arrays.stream(bookClass.getFields()).forEach(System.out::println);
}
}
public class Main {
public static void main(String[] args) {
Class<Book> bookClass = Book.class; // .class
Arrays.stream(bookClass.getDeclaredFields()).forEach(System.out::println);
}
}
Class.getConstructor(), constructuer.newInstance()
클래스 인스턴스에서 해당 클래스의 생성자에 접근할 수 있는데 이때 클래스에 맞는 파라미터 타입이나 갯수가 맞지 않으면 Exception이 터진다.
public class Main {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<Book> bookClass = Book.class; // .class
Constructor<Book> constructor = bookClass.getConstructor(null);
Book book = constructor.newInstance();
}
}
생성자
Class 타입의 인스턴스에서 .getConstructors()를 통해 생성자 리스트를 가져오거나 .getConstructer(parameter)를 통해 원하는 생성자를 반환 받을 수 있다.
newInstance(parameter)
생성자에 필요한 파라미터를 넘겨주면 인스턴스를 생성할 수 있다.
현재는 기본 생성자를 이용하기 위해 null을 사용했고 파라미터를 넘겨주지 않고 인스턴스를 생성했다.
만약 getConstructor로 생성자를 가져올때 파라미터 타입이나 갯수가 맞지 않으면 NoSuchMethodException이 발생,
만든 생성자에 맞지 않는 파라미터를 넘겨주면 IllegalArgumentException이 발생
setAccesible
public class Main {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
Class<Book> bookClass = Book.class; // .class
Constructor<Book> constructor = bookClass.getConstructor(null);
Book book = constructor.newInstance();
Field a = bookClass.getDeclaredField("a");
a.setAccessible(true);
System.out.println(a.get(book));
a.set(book, "b");
System.out.println(a.get(book));
}
}
Book 클래스 인스턴스에 접근해 Book 인스턴스를 만들면 필드값들이 초기화되는데 이때 private 접근제한자를 갖고있는 a 필드도 클래스 인스턴스의 setAcessible 값을 true로 주면 접근할 수 있게 바뀌어 get으로 값을 가져올 수도 있고 set으로 값을 바꿀 수도 있다.
클래스 인스턴스에 접근해 Declared + setAccesible 조합으로 접근제한자에 상관없이 클래스 정보를 조작, 실행할 수 있지만 클래스를 생성해서 실행하려면 인스턴스의 생성자에도 접근을 해야하는데 이때 클래스마다 다른 생성자의 파라미터 타입이나 갯수를 파악할수 없으니기본 생성자로 일단 클래스를 생성하고 리플렉션 API로 조작하기 위해서 많은 라이브러리, 프레임워크에서 기본생성자를 요구하는 것 같다.
간단한 setter 의존성 주입을 리플렉션 API로 구현하기
@AutoWired 대신할 @Inject
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
}
Application Context(Bean Container) 대신할 ContainerService
public class ContainerService {
public static <T> T getObject(Class<T> type){
T instance = createInstance(type);
Arrays.stream(type.getDeclaredFields()).forEach(f->{
if(f.getAnnotation(Inject.class)!=null){
Object filedInstance = createInstance(f.getType());
f.setAccessible(true);
try {
f.set(instance, filedInstance);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
});
return instance;
}
private static <T> T createInstance(Class<T> type) {
try {
return type.getConstructor(null).newInstance();
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
생성할 클래스의 필드에 붙은 어노테이션 중에 @Inject를 찾아 필드 주입을 한 인스턴스 반환
Repository
public class BookRepository {
}
Service
public class BookService {
@Inject
BookRepository bookRepository;
}
ServiceTest
class ContainerServiceTest {
@Test
public void test(){
BookRepository bookRepository = ContainerService.getObject(BookRepository.class);
// applicationContext.getBean(BookRepository.class);
Assertions.assertNotNull(bookRepository);
}
@Test
public void test2(){
BookService bookService = ContainerService.getObject(BookService.class);
Assertions.assertNotNull(bookService);
Assertions.assertNotNull(bookService.bookRepository);
}
}
싱글톤 컨테이너도 아니고, 세터 주입 밖에 안되지만 간단하게 리플렉션 API가 뭔지 어떻게 쓰는지 알아봤다.
왜 필요한가
접근제한자를 무시하고 클래스를 조작해서 객체지향의 캡슐화가 지켜지지 않는데 왜 필요한가?
작은 규모의 프로젝트는 개발자가 컴파일 타임에 객체의 의존관계를 다 파악하고 쓰지만 프레임워크를 이용해서 개발할때는 객체의 생성, 소멸, 의존성 관계의 책임을 프레임워크가 지게된다.
스프링부트로 만든 톰캣 내장 자바 애플리케이션을 실행하면 서블렛 컨테이너가 메모리에 올라갈 때 @ComponentScan에 의해 @Service, @Repository, @Component 들이 루트 애플리케이션 컨텍스트에 올라가고, 디스패쳐 서블렛과 묶이는@Controller, Interceptor, ViewResolver 등의 객체들은 웹 최초 요청이 들어오면 서블렛 애플리케이션 컨텍스트에 올라가는 등 프레임워크가 필요할 때 객체를 만들어 메모리에 올리고 빈 생명주기 및 의존성을 관리하는 등의 작업이 이루어진다.
따라서 런타임에 객체생성이 필요하고 리플렉션 API로 어노테이션에 맞는 것들이 추가된 프록시 객체로 프로젝트를 관리하기 때문에 필요하다고 보면된다.
하지만 무분별한 리플렉션 API 사용은 아래와 같은 단점을 잘 알고 사용해야한다.
- 컴파일 시점의 자료형 검사를 활용할 수 없다
- 가독성이 떨어진다
- Heap에서 GC되지 않고 계속 쌓여 Out of Memory Error를 유발할 수 있다
- 접근 제한자 무시하고 접근할 수 있다
참고
https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html
https://www.inflearn.com/course/the-java-code-manipulation/dashboard
https://lkhlkh23.tistory.com/95