Java Reflection 정리
인프런 '더 자바 - 코드를 조작하는 다양한 방법' 백기선님 강의내용 정리
Reflection
코드를 조작하는 다양한 방법 중 하나.
- 정의된 클래스 / 메소드 / annotation 정보를 확인하고 참조하기
- 인스턴스를 생성하고, 필드값을 변경하며, 메소드를 실행할 수 있는 방법.
스프링부트 프로젝트 생성. dependency에 스프링 웹 추가.
BookService와 BookRepository라는 두 개의 컴포넌트를 생성하고,
BookService에 Autowired로 BookRepository를 추가한 뒤
BookServiceTest 코드를 아래처럼 작성한다.
package com.inspirit941.thejavaspringreflection;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class BookServiceTest {
@Autowired
BookService bookService;
// 스프링이 bookRepository 생성해서 bookService에 DI 해주었음을 확인하는 코드
// 생성해서 DI를 넣었기 때문에 NotNull이 True를 반환한다.
@Test
public void di(){
assertNotNull(bookService.bookRepository);
}
}
테스트를 통과한다. 즉 BookService 클래스에 BookRepository가 성공적으로 DI 완료됨. 이걸 스프링에서 어떻게 하는지가 Reflection 기능의 핵심이다.
Reflection API 1
maven architype quickstart로 새 프로젝트 생성.
reflection을 사용하려면 class Class< T > 문서를 확인한다.
reflection으로 원하는 정보를 참조하는 방법
package com.inspirit941;
public class Book {
private static String staticName = "staticName";
private static final String staticFinalName = "staticFinalName";
private String privateName = "privateName";
public String publicName = "publicName";
public Book() {
}
public Book(String privateName, String publicName) {
this.privateName = privateName;
this.publicName = publicName;
}
private void privateVoidFunc() {
}
public void publicVoidFunc() {
}
public int publicIntFunc(){
return 100;
}
}
/// 인터페이스 정의
package com.inspirit941;
public interface MyInterface {
}
// 상속받은 구현체 클래스 정의
package com.inspirit941;
public class MyBook extends Book implements MyInterface {
}
package com.inspirit941;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
public class App
{
public static void main( String[] args ) throws ClassNotFoundException {
// 생성한 객체에 접근하려면 class가 필요. 클래스 인스턴스에 접근하는 방법은 아래와 같다.
// 1. 클래스 로딩이 끝나면, 클래스 타입의 인스턴스를 heap에 저장한다.
// 클래스를 로딩만 해도 인스턴스가 생성됨. 클래스 타입의 인스턴스를 가져오는 방법은 아래와 같음.
Class<Book> bookClass = Book.class;
// 인스턴스가 이미 있을 경우, getClass()로 클래스 타입을 가져올 수 있다.
Book book = new Book();
book.getClass();
// FQDN만 알고 있다면 (예컨대 com.inspirit941.Book 문자열만 알고 있으면)
// 클래스 타입의 인스턴스를 Class.forName으로 찾을 수 있다. 클래스 없으면 notfoundException.
// 패키지 경로의 클래스명을 입력하는 걸 스프링에서 해봤다 -> 이런 식으로 내부동작할 가능성이 높음.
Class<?> aClass1 = Class.forName("com.inspirit941.Book");
// 참조할 수 있는 정보들.
// 필드 배열 확인하기.
Field[] fields = bookClass.getFields();
Arrays.stream(bookClass.getFields()).forEach(System.out::println);
// return publicName. 필드 하나만 나온다.
System.out.println("======");
// getFields() 메소드는 public Scope만 리턴하기 때문.
// 필드를 다 가져오려면 getDeclaredFields("특정한 필드명만 가져오려면 여기에 입력");
Arrays.stream(bookClass.getDeclaredFields()).forEach(field -> {
try {
// 접근제한자 속성을 풀려면 true 입력. reflection으로는 이렇게 접근지시자를 무시할 수 있음.
field.setAccessible(true);
// 실제로 필드를 가져오려면, 인스턴스가 있어야 한다. get(book)으로 인스턴스를 불러옴.
System.out.printf("%s %s\n", field, field.get(book));
// getModifiers() 메소드로 해당 메소드의 scope를 알 수 있다.
int modifiers = field.getModifiers();
System.out.println(Modifier.isPrivate(modifiers));
System.out.println(Modifier.isStatic(modifiers));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
});
// 메소드 가져오기
// 직접 정의한 것 외에도 Object에서 상속받은 메소드도 전부 출력
Arrays.stream(bookClass.getDeclaredMethods()).forEach(System.out::println);
// 생성자: getDeclaredConstructors(), 부모클래스: getSuperclass()
System.out.println("부모클래스 가져오기");
System.out.println(MyBook.class.getSuperclass());
// 인터페이스 : getInterfaces()
Arrays.stream(MyBook.class.getInterfaces()).forEach(System.out::println);
}
}
reflection과 annotation
중요 Annotation
- @Retention : 해당 annotation을 언제까지 유지할 것인가 (소스, 클래스, 런타임)
- @Inherit : 하위 클래스까지 어노테이션을 전달할 것인가
- @Target : 어디에 사용할 수 있는가
Annotation은 인터페이스를 정의할 때 @Interface로 변경하면 바로 사용할 수 있고, 클래스 정의부분 바로 위에 붙여서 사용한다.
public @interface MyAnnotation {
}
@MyAnnotation
public class Book {
//...
}
바이트코드에서 annotation 정보까지 보려면 javap -c -v 클래스_절대경로.class
를 터미널에 입력하면 됨.
package com.inspirit941;
import java.lang.annotation.*;
// Retention의 기본값은 Class. 실행 시까지 정보가 유지되려면 Runtime
@Retention(RetentionPolicy.RUNTIME)
// @Target으로 해당 Annotation의 적용가능한 범위 설정. type과 field만 설정할 경우 생성자 / 메소드 등에는 불가능.
@Target({ElementType.TYPE, ElementType.FIELD})
// 만약 MyBook에서도 Book에 정의된 annotation정보를 가져와야 할 경우에 @Inherited 명시.
@Inherited
public @interface MyAnnotation {
// annotation에는 제한된 타입의 값만 정의할 수 있다. primitive type & reference, list 등.
// 디폴트 값 설정도 가능. 기본값 정의하지 않으면, Annotation을 사용하는 곳에서 반드시 값을 입력해야 함.
// 여기 정의된 필드명은 기본적으로 annotation 사용하는 측에서 값을 할당할 때 명시해야 한다.
// @MyAnnotation(value = "name", number = 10) 처럼
String value() default "donggeonlee";
int number() default 100;
}
package com.inspirit941;
import java.lang.reflect.Array;
import java.util.Arrays;
public class testAnnotation {
public static void main(String[] args) {
// annotation을 조회하려고 아래 코드를 실행하면, 기본적으로는 아무것도 보이지 않는다.
Arrays.stream(Book.class.getAnnotations()).forEach(System.out::println);
System.out.println("======");
// Annotation 자체는 주석과 같은 취급이기 때문.
// 소스, 클래스까지는 해당 정보가 남지만, 바이트코드를 로딩했을 때 메모리에는 남지 않기 때문.
// 런타임에도 이 값을 유지하려면 @Retention()에 Runtime을 추가해야 한다.
// 사용할 수 있는 위치를 지정하는 @Target.
// MyAnnotation에 @Inherited가 붙어 있으면
// MyBook에서 getAnnotation을 수행해도 annotation값을 볼 수 있다.
// 상속받은 거 말고 해당 클래스에 정의된 것만 불러오고 싶으면 getDeclaredAnnotations()
Arrays.stream(MyBook.class.getAnnotations()).forEach(System.out::println);
System.out.println("======");
// 필드 / 메소드에 붙은 Annotation을 확인하려면?
// ex) 필드에 붙은 Annotation설정 가져오기
Arrays.stream(Book.class.getDeclaredFields()).forEach(f -> {
Arrays.stream(f.getAnnotations()).forEach(annotation -> {
System.out.println(annotation);
// return MyAnnotation(value = "publicNameField", number = 10)
// 필드에 붙은 Annotation은 Annoation() 타입.
// 내부에 정의된 값을 따로 조회하거나 참조할 수 있음.
if (annotation instanceof MyAnnotation) {
MyAnnotation myAnnotation = (MyAnnotation) annotation;
System.out.println(myAnnotation.value());
System.out.println(myAnnotation.number());
}
});
});
}
}
이 Reflection을 사용해서
- 실제 인스턴스를 만들고
- 필드값 변경 / 메소드 실행이 가능하다.
Reflection API 2
book2 클래스 정의
package com.inspirit941;
public class Book2 {
private static String staticName = "staticName";
private static final String staticFinalName = "staticFinalName";
private String privateName = "privateName";
public String publicName = "publicName";
public Book2() {
}
public Book2(String privateName, String publicName) {
this.privateName = privateName;
this.publicName = publicName;
}
private void privateVoidFunc() {
}
public void publicVoidFunc() {
System.out.println(publicName);
}
public int publicIntFunc(int left, int right){
return left + right;
}
}
reflection으로
- 생성자 사용해 인스턴스 생성하기
- 필드값 가져오고 수정하기
- 메소드 가져오고 실행하기
package com.inspirit941;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class testAnnotation2 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
// 1. 인스턴스 생성하기.
Class<?> bookClass = Class.forName("com.inspirit941.Book2");
// 기본 권장사항은 Constructor 사용. -> 기본생성자일 경우 null을 파라미터로 입력.
Constructor<?> constructor = bookClass.getConstructor(null);
Book2 defaultBook2 = (Book2) constructor.newInstance();
System.out.println(defaultBook2);
// 문자열 입력받는 생성자를 가져와야 할 경우 String.class를 파라미터로 입력.
Constructor<?> stringConstructor = bookClass.getConstructor(String.class, String.class);
Book2 stringBook = (Book2) stringConstructor.newInstance("privateName", "publicName");
System.out.println(stringBook);
// 필드값 가져오기
// 1. static : 모든 클래스가 같이 쓰는 변수이므로 특정 오브젝트를 넘겨줄 필요가 없음.
// get 메소드 안에 null입력.
Field staticName = Book2.class.getDeclaredField("staticName");
// private이라서 accessible 설정.
staticName.setAccessible(true);
System.out.println(staticName.get(null));
// 값 변경하기.
staticName.set(null, "setStaticNameOnMainMethod");
System.out.println(staticName.get(null));
// 2. 특정 인스턴스의 필드 가져오기.
Field publicName = Book2.class.getDeclaredField("publicName");
// 인스턴스가 있어야 가져올 수 있는 값. 위에서 정의한 stringBook을 인자로 가져온다.
System.out.println(publicName.get(stringBook));
publicName.set(stringBook, "setPublicFieldByMainMethod");
System.out.println(publicName.get(stringBook));
// 3. 메소드 가져오기.
// 메소드를 실행하려면 invoke(). 파라미터로는 메소드를 실행할 인스턴스 & 메소드 실행에 필요한 파라미터.
Method publicVoidFunc = Book2.class.getDeclaredMethod("publicVoidFunc");
publicVoidFunc.invoke(stringBook);
// 메소드 파라미터가 있다면, 파라미터도 같이 인풋으로 지정한다. 여기서 primitive / reference 타입을 구분해야 한다.
Method publicIntFunc = Book2.class.getDeclaredMethod("publicIntFunc", int.class, int.class);
int returnValue = (int) publicIntFunc.invoke(stringBook, 1, 2);
System.out.println(returnValue);
}
}
DI 프레임워크 생성하기
@Inject라는 Annotation으로 필드값을 주입하는 컨테이너 생성하기.
di라는 하위패키지를 생성하고, Inject Annotation과 ContainerService 등 구현에 필요한 객체를 생성한다. 테스트를 위해 ContainerServiceTest도 생성.
cf. Test 디렉토리에 있는 객체는 src 참조가 가능하지만, 그 역은 성립하지 않는다.
Test 디렉토리
package com.inspirit941.di;
public class BookService {
// 필드 주입
@Inject
BookRepository bookRepository;
}
package com.inspirit941.di;
public class BookRepository {
}
package com.inspirit941.di;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class ContainerServiceTest {
@Test
// inject 쓰지 않은 객체.
public void getObject_BookRepository(){
BookRepository bookRepository = ContainerService.getObject(BookRepository.class);
assertNotNull(bookRepository);
}
@Test
// inject로 객체를 주입한 객체를 생성.
public void getObject_BookService(){
BookService bookService = ContainerService.getObject(BookService.class);
assertNotNull(bookService);
assertNotNull(bookService.bookRepository);
}
}
src 코드
package com.inspirit941.di;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
// 런타임에 참조해야 하므로
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
}
package com.inspirit941.di;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
public class ContainerService {
// Generic Type 선언. 클래스 타입이 들어오면, 해당 클래스의 인스턴스를 리턴하도록.
// 테스트 코드에 정의된 객체를 리턴하기 위해 reflection 사용.
public static <T> T getObject(Class<T> classType) {
T instance = createInstance(classType);
// 생성한 인스턴스의 필드를 확인한다.
Arrays.stream(classType.getDeclaredFields()).forEach(f -> {
Inject annotation = f.getAnnotation(Inject.class);
if (annotation != null){
// BookService객체를 생성할 경우, 여기에는 @inject로 주입받은
// BookRepository 객체가 getType() 메소드의 리턴값이 된다.
Object annotationInstance = createInstance(f.getType());
// 접근 가능하도록 설정해놓고
f.setAccessible(true);
try {
// 객체 주입.
f.set(instance, annotationInstance);
} catch (IllegalAccessException e) {
throw new RuntimeException();
}
}
});
return instance;
}
public static <T> T createInstance(Class<T> classType) {
try {
return classType.getConstructor(null).newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException();
}
}
}
이렇게 생성한 IoC 컨테이너 사용하기
mvn install
실행. 실행하면 로컬 maven에 jar파일이 들어간다.- 새 프로젝트를 생성하고, 저장한 프로젝트의 groupId / artifactId / version값을 dependency에 넣는다.
mvn package
를 새 프로젝트 터미널에서 실행했을 때 External Library에 배포한 프로젝트 jar파일이 있으면 됨.
cf. version에 에러표시가 뜰 수 있는데, snapshot을 지우고 1.0.0.RELEASE 같은 이름으로 바꾸고 1부터 진행한다.
훨씬 복잡하고 정교하게 이루어져 있지만, 스프링이 DI를 수행하는 방법의 요체는 위와 같은 reflection을 활용한 객체 생성 및 주입이다.
정리 및 활용
reflection으로
- 클래스의 인스턴스 생성
- 필드 세팅 / 메소드 실행 등이 가능함.
사용처
- 스프링은 의존성 주입에 사용하고,
- MVC의 경우 View에서 넘어오는 데이터를 특정 객체에 바인딩할 때,
- Hibernate에서도 특정 Entity에 setter가 없으면 해당 필드에 값을 바로 주입(설정)한다.
- JUnit의 경우 아에 ReflectionUtils라는 클래스를 내부적으로 정의해서 씀.
유의점
- 기능은 강력하지만, 지나치게 / 잘못 쓸 경우 성능이슈 발생.
- 예컨대 인스턴스가 이미 생성되어 있고 그걸 계속 활용할 수 있으면, reflection으로 접근하는 방법은 불필요함.
- 컴파일에서는 확인되지 않고, 런타임에서만 발생하는 문제가 발생.
- 접근지시자를 무시할 수 있다. (캡슐화 무시)
잘못 쓰면 문제라는 거지, 리플렉션 자체가 잘못되었다는 건 아님. 유의!