[java] Reflection
zl존석동
·2022. 2. 14. 19:19
처음 알게 된 Java Reflection 개념을 맛보고 간단하게 테스트 해보며 이런게 있구나 라는 것을 알아가는 목적으로 공부하고 기록해보았다.
Reflection 이란
리플렉션이란 객체를 통해 클래스의 정보를 분석하는 프로그램 기법을 말한다.
구체적인 클래스 타입을 알지 못해도 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API 이다.
Class, Constructor, Method, Field 정보를 가져와 객체를 생성하거나 메소드를 호출하거나 멤버 값을 변경할 수 있다.
컴파일 시점이 아니라 런타임 시점에 동적으로 특정 클래스의 정보를 추출해줄 수 있다.
언제 쓰나
동적으로 클래스를 사용할 때
컴파일 시점에는 어떤 클래스를 사용할지 모르나 런타임 시점에 특정 클래스를 가져와 실행해야 하는 경우
Spring Framework 의 BeanFactory가 애플리케이션이 실행 된 후 객체가 호출될 때 객체의 인스턴스를 동적으로 생성하게 된다.
리플렉션을 통해 런타임 시점에 개발자가 등록한 빈을 주입해 사용할 수 있게 한다.
리플렉션 테스트를 위한 준비
맨 아래 레퍼런스의 예시를 참고해 간단한 두 개의 클래스를 만들었다.
package reflection;
public class Parent {
private String name;
private int age;
public Parent() {
this.name = "parent";
this.age = 25;
}
public Parent(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
private void hello() {
System.out.println("HELLO");
}
public int getAge() {
return age;
}
}
package reflection;
public class Child extends Parent {
private String address;
public String hello = "hello";
public Child() {
this.address = "주소";
}
private Child(String address) {
this.address = address;
}
private void childHello() {
System.out.println("HELLO_CHILD");
}
public void childHell() {
System.out.println("HELL_CHILD");
}
@Override
public String getName() {
return super.getName() + "child다";
}
}
클래스 정보 가져오기
클래스 또는 인터페이스를 가리키는 java.lang.Class 클래스를 사용할 것이다
해당 클래스의 문서 설명을 번역하여 가져와봤다.
Class 는 동작중인 자바 애플리케이션에서 클래스들과 인터페이스들을 표현해주는 인스턴스다.
클래스의 한 종류인 enum과 인터페이스의 한 종류인 annotation 또한 매한가지다.
모든 array 또한 클래스에 속해 해당 클래스로 reflect 될 수 있을 뿐 아니라 자바 primitive 타입 과 void 키워드 또한 해당 객체로 나타날 수 있다
애플리케이션에 특정 클래스가 존재함을 알고 있을 때
/**
* 클래스의 존재를 알고 있을 때
*/
static void findChildClassWithCode() {
Class<Child> clazz = Child.class;
System.out.println("ClassName:" + clazz.getName());
// Output
// ClassName:reflection.Child
}
특정 클래스의 이름만 알고 있을 때
패키지 경로 정보가 포함된 클래스 이름으로 입력해야 한다.
/**
* 클래스의 이름만 알고 있을 떄
*
* @param className: 패키지 네임이 포함된 클래스 이름
*/
static void findClassByName(String className) {
try {
Class<?> clazz = Class.forName(className);
System.out.println("ClassName:" + clazz.getName());
// Output
// ClassName:reflection.Child
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
패키지 경로 정보가 포함된 클래스 이름으로 입력해야 한다.
생성자 찾기
java.lang.reflect.Constructor 클래스를 이용해 클래스의 생성자를 찾아보자
클래스의 인자 없는 생성자 찾기
/**
* 클래스의 인자 없는 생성자를 출력한다.
*
* @param clazz: 클래스 정보
*/
static void findNoArgsConstructorFromClass(Class<?> clazz) {
try {
// 인자 없는 생성자
Constructor<?> constructor = clazz.getDeclaredConstructor();
System.out.println("constructor: " + constructor.getName());
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
클래스의 인자 있는 생성자 찾기
/**
* 클래스의 인자 있는 생성자를 출력한다.
*
* @param clazz: 클래스 정보
* @param args : 클래스 생성자의 인자들 타입 클래스 정보
*/
static void findSpecificArgsConstructorFromClass(Class<?> clazz, Class<?>... args) {
// 인자 있는 생성자
try {
Constructor<?> constructor = clazz.getDeclaredConstructor(args);
System.out.println("constructor: " + constructor.getName());
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
// Output
// constructor: reflection.Parent
}
클래스의 모든 생성자 찾기
/**
* 클래스로부터 모든 생성자 정보를 출력한다.
*
* @param clazz : 클래스 정보
*/
static void findAllConstructorFromClass(Class<?> clazz) {
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
System.out.print("constructors : ");
for (Constructor<?> cons : constructors) {
System.out.print(cons + " ");
}
System.out.println();
// Output
// constructors : public reflection.Child() private reflection.Child(java.lang.String)
}
메소드 찾기
java.lang.reflect.Method 클래스를 활용한다는 점을 제외하고 생성자 찾기와 비슷하다.
클래스의 특정 메소드 찾기
/**
* 클래스의 특정 메소드를 찾아 출력한다.
*
* @param clazz: 클래스 정보
* @param methodName: 찾으려는 메소드 이름
* @param args: 메소드 파라미터 목록
*/
static void findSpecificMethodFromClass(Class<?> clazz, String methodName, Class<?>... args) {
try {
Method method = clazz.getMethod(methodName, args);
System.out.println("method:" + method.getName());
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
위의 코드에서는 getMethod 메소드로 찾았는데 public 인 메소드만 찾을 수 있으며 상속받은 메소드도 찾을 수 있다.
findSpecificMethodFromClass(Child.class, "childHello"); // Exception!!
findSpecificMethodFromClass(Child.class, "getName"); // OK (재정의함)
findSpecificMethodFromClass(Child.class, "getAge"); // parent 의 메소드: OK
이를 getDeclaredMethod로 변경한다면 결과는 다음과 같다.
findSpecificMethodFromClass(Child.class, "childHello"); // OK
findSpecificMethodFromClass(Child.class, "getName"); // OK (재정의함)
findSpecificMethodFromClass(Child.class, "getAge"); // super 클래스의 메소드 안됨 Exception!!
Field 찾기는 위와 같으니 생략하였다.
메소드 호출하기
클래스로부터 메소드 정보를 가져와 객체의 메소드를 호출할 수 있다.
Method.invoke() 로 호출할 수 있고 setAccessible(true) 설정을 통해 private 메소드도 호출할 수 있다.
/**
* @param instance: parent 계열의 인스턴스
* @param methodName : 수행할 메소드 이름
* @param args: 메소드 파라미터
* @param <T>
*/
static <T extends Parent> void invokeSpecificMethodFromClass(T instance, String methodName, Class<?>... args) {
try {
Method method = instance.getClass().getDeclaredMethod(methodName);
method.setAccessible(true);
method.invoke(instance, args);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
setAccessible(true) 를 통해 접근이 제한된 메소드의 접근도를 변경하고 수행할 수 있다는 점에 대해 뭔가 굉장히 쎄하다는 생각이 들었다.
리플렉션을 통해 개발자가 의도한 api 동작을 파괴시켜버릴 수 있는게 아닌가?? 라는 생각이 들었다.
IDE의 정적 코드 분석 플러그인에서도 역시 경고를 해준다.

설명을 보면 정의된 객체에 대해 리플렉션을 통해 임의로 변경하여 접근한다면 캡슐화 원칙을 깨뜨려 런타임에 터질 수 있으니 하지말라는 것으로 보인다.
이펙티브 자바에서도 되도록 생성에만 사용하고 그 외 핸들링에는 리플렉션을 지양하고 인터페이스를 사용하라고 말해주던 아이템이 있었던 것이 기억난다.
아직 해당 내용을 상세히 보진 않았지만 이런 상황들 때문에 런타임에 위험하기 때문이 아닌가 생각한다.
리플렉션을 통해 생성자로 인스턴스화도 해보자
/**
* 주어진 parent 객체를 통해 새 인스턴스를 생성한다.
* @param instance: parnet 객체
*/
static void getNewInstanceFromClass(Parent instance) {
try {
Constructor<Parent> constructor = (Constructor<Parent>) instance.getClass().getDeclaredConstructor();
Parent parent = constructor.newInstance();
System.out.println(instance == parent);
// Output
// false
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
newInstance() 메소드를 통해 인스턴스로부터 새로운 인스턴스를 만드는 형태이다.
여기서도 setAccessible(true) 메소드를 이용해 private 생성자를 통해 새로 인스턴스화 할 수 있는데
하나만 생성되서 전역적으로 사용됨이 의도된 (enum이 아니라 클래스로 구성한)싱글턴 패턴의 인스턴스에도 이게 적용되어 버리기 때문에 새로운 인스턴스가 만들어지는 문제가 발생할 수 있다.
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
...
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
...
}
Constructor 클래스의 newInstance 메소드를 확인해보면 enum 의 경우는 리플렉션으로 새롭게 인스턴스화 되는 것이 방어 처리 되어있다.
필드 값 변경하기
/**
* 입력받는 인스턴스의 특정 필드의 값을 변경한다.
*
* @param instance : 인스턴스
* @param fieldName : 변경하려는 멤버변수
* @param changeValue : 변경하고자하는 값
*/
static void modifyFieldFromClass(Object instance, String fieldName, Object changeValue) {
Class<?> clazz = instance.getClass();
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
System.out.println("before change:" + field.get(instance));
field.set(instance, changeValue);
System.out.println("after change:" + field.get(instance));
// Output
// before change:parent
// after change:kikiki
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
모두 private 접근자를 가지는 멤버로 구성되어있으며 수정 관련 동작이 없는 Parent 클래스의 인스턴스를 파라미터로 넣어도 직접 내부 값을 수정할 수 있다.
필드를 직접 수정하는 field.set(instance, changeValue) 부분 또한 정적 코드 분석 플러그인에서 우회 수정이라며 제거하라고 경고해주고 있다.
느낀 점
런타임 시에 동적으로 클래스 관련 정보로 부터 인스턴스를 만들고 조작할 수 있는 강력하고 유연한 기능이지만
그렇기에 잘못 사용하고 잘못 설계하면 런타임 시에 치명적인 오류를 발생시킬 수 있는 위험한 기능이기도 한 것 같다.
MyBatis를 통해 정의한 클래스와 Sql 쿼리 간 매핑을 하거나 Mvc controller에서 요청 사용자 json 데이터를 얻어와 객체로 변환해주는 Jackson 라이브러리를 사용할 때
기본 생성자라던가 멤버변수에 대응하는 정확한 명칭을 가지는 Getter 가 없으면 런타임에서 활용할 때 오류가 났던 기억이 있었는데 왜 그랬고 뭘 이용했는지 리플렉션을 공부해보면서 짐작이 되는 것 같아 신기하다
어렴풋이 기억나는 것이라 정확한 조건이나 상황은 아닐테지만 나중에 사용해보게 되면 다시 예외를 확인하며 까뒤집어서 확인해봐야겠다!
Ref
Java - Reflection 쉽고 빠르게 이해하기
자바의 리플렉션(Reflection)은 클래스, 인터페이스, 메소드들을 찾을 수 있고, 객체를 생성하거나 변수를 변경할 수 있고 메소드를 호출할 수도 있습니다. Reflection은 Class, Constructor, Method, Field와 같
codechacha.com
'Java' 카테고리의 다른 글
| [MyBatis] INSERT 성공 후 생성된 자원의 PK 같이 얻어오기 (0) | 2022.02.26 |
|---|---|
| Annotation (0) | 2022.02.20 |
| [Java] ConcurrentModificationException 해결하기 (0) | 2022.02.04 |
| [Java] 올바른 Map Iteration (0) | 2022.01.23 |
| [Java] 예외란 뭘까 (0) | 2022.01.10 |