리플렉션(Reflection)이란?
리플렉션은 구체적인 클래스 타입을 알지 못하더라도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API를 말하며,
컴파일 시간이 아닌 실행 시간에 동적으로 특정 클래스의 정보를 추출할 수 있는 프로그래밍 기법이라 할 수 있다.
언제 사용할까?
- 동적으로 클래스를 사용해야 할 때 사용한다.
- 다시 말해, 작성 시점에는 어떠한 클래스를 사용해야 할지 모르지만 런타임 시점에서 가져와 실행해야 하는 경우 필요하다.
- 프레임워크나 IDE에서 이런 동적 바인딩을 이용한 기능을 제공한다.
리플렉션을 통해 알 수 있는 정보
- Class
- Constructor
- Method
- Field
리플렉션 예시
public class Parent {
private String str1 = "str1";
public String str2 = "str2";
public Parent() {
}
private void method1() {
System.out.println("method1");
}
public void method2(int n) {
System.out.println("method2" + n);
}
public void method3() {
System.out.println("method3");
}
}
Parent 클래스이다. private, public 접근자의 메서드와 변수가 존재한다.
public class Child extends Parent{
public String cstr1 = "cstr1";
private String cstr2 = "cstr2";
public Child() {
}
private Child(String str1) {
cstr1 = str1;
}
public int method4(int n) {
System.out.println("method4: " + n);
return n;
}
private int method5(int n) {
System.out.println("method5: " + n);
return n;
}
public int method6(int n, String s) {
System.out.println("method6" + n +"str ="+ s);
return n;
}
}
Parent를 상속받은 Child 클래스이다. Parent와 마찬가지로 private, public접근자의 메서드와 변수가 존재한다.
Class 찾기
- 클래스. class로 가져오기
- 인스턴스. getClass()로 가져오기
- Class.forName("클래스명")으로 가져오기
@Test
public void classTest() {
Class clazz = Child.class;
Assertions.assertEquals(clazz, Child.class);
}
@Test
public void classTest2() {
Child child = new Child();
Class clazz = child.getClass();
Assertions.assertEquals(clazz, Child.class);
}
@Test
public void classTest3() throws ClassNotFoundException {
Class clazz = Class.forName("reflection.Child");
Assertions.assertEquals(clazz, Child.class);
}
위와 같은 방식으로 클래스를 찾을 수 있다.
Constructor 찾기
- 기본 생성자 - getDeclaredConstructor()
- (String) 매개변수가 있는 생성자 - getDeclaredConstructor(String.class)
- 모든 생성자 - getDeclaredConstructors()
- 접근자가 public인 모든 생성자 - getConstructors()
//기본 생성자
@Test
public void constructorTest() throws ClassNotFoundException, NoSuchMethodException {
Class clazz = Class.forName("reflection.Child");
Constructor constructor = clazz.getDeclaredConstructor();
System.out.println(constructor); //public reflection.Child()
}
//String을 매개변수로 갖는 생성자
@Test
public void constructorTest2() throws ClassNotFoundException, NoSuchMethodException {
Class clazz = Class.forName("reflection.Child");
Constructor constructor = clazz.getDeclaredConstructor(String.class);
System.out.println(constructor); //private reflection.Child(java.lang.String)
}
//모든 생성자
@Test
public void constructorTest3() throws ClassNotFoundException {
Class clazz = Class.forName("reflection.Child");
Constructor[] constructor = clazz.getDeclaredConstructors();
Arrays.stream(constructor).forEach(System.out::println);
//public reflection.Child()
//private reflection.Child(java.lang.String)
}
//접근자가public인 모든 생성자
@Test
public void constructorTest4() throws ClassNotFoundException{
Class clazz = Class.forName("reflection.Child");
Constructor[] constructor = clazz.getConstructors();
Arrays.stream(constructor).forEach(System.out::println);
//public reflection.Child()
}
여러 가지 방식으로 생성자를 찾을 수 있다.
Method 찾기
- 인자에 해당 메서드에 해당하는 인자클래스를 넣고, 없으면 null 값을 넣는다. - getDeclaredMethod()
- 인수가 여러 개라면 Class배열에 담아서 넘긴다.
- 모든 public 메서드 - getMethods()
//인자가 있는 경우 알맞은 인자를 넣어준다.
@Test
public void methodTest() throws ClassNotFoundException, NoSuchMethodException {
Class clazz = Class.forName("reflection.Child");
Method method = clazz.getDeclaredMethod("method4", int.class);
System.out.println(method); //public int reflection.Child.method4(int)
}
//인자가 없는 경우 null을 넣어준다.
@Test
public void methodTest2() throws ClassNotFoundException, NoSuchMethodException {
Class clazz = Class.forName("reflection.Parent");
Method method = clazz.getDeclaredMethod("method1", null);
System.out.println(method); //private void reflection.Parent.method1()
}
//인자가 두 개 이상인 경우
@Test
public void methodTest3() throws ClassNotFoundException, NoSuchMethodException {
Class clazz = Class.forName("reflection.Child");
Class[] partTypes = new Class[2];
partTypes[0] = int.class;
partTypes[1] = String.class;
Method method = clazz.getDeclaredMethod("method6", partTypes);
System.out.println(method); //public int reflection.Child.method6(int,java.lang.String)
}
//public메서드만
@Test
public void methodTest4() throws ClassNotFoundException, NoSuchMethodException {
Class clazz = Class.forName("reflection.Child");
Method[] method = clazz.getMethods();
Arrays.stream(method).forEach(System.out::println);
// public int reflection.Child.method4(int)
// public int reflection.Child.method6(int,java.lang.String)
// public void reflection.Parent.method3()
// public void reflection.Parent.method2(int)
// public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
// public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
// public final void java.lang.Object.wait() throws java.lang.InterruptedException
// public boolean java.lang.Object.equals(java.lang.Object)
// public java.lang.String java.lang.Object.toString()
// public native int java.lang.Object.hashCode()
// public final native java.lang.Class java.lang.Object.getClass()
// public final native void java.lang.Object.notify()
// public final native void java.lang.Object.notifyAll()
}
getMethods()를 사용할 시 부모타입의 public 모든 메서드도 호출되는 것을 볼 수 있다.
Field 찾기
- 변수 이름으로 찾기 - getDeclaredField()
- public 필드 찾기 - getFields()
//필드 이름으로 찾기
@Test
public void fieldTest() throws ClassNotFoundException, NoSuchFieldException {
Class clazz = Class.forName("reflection.Child");
Field field = clazz.getDeclaredField("cstr2");
System.out.println(field); //public java.lang.String reflection.Child.cstr2
}
//public 필드 찾기
@Test
public void fieldTest2() throws ClassNotFoundException, NoSuchFieldException {
Class clazz = Class.forName("reflection.Child");
Field[] field = clazz.getFields();
Arrays.stream(field).forEach(System.out::println);
// public java.lang.String reflection.Child.cstr1
// public java.lang.String reflection.Parent.str2
}
getFields를 사용할 경우 부모의 public 필드까지 나오게 된다.
필드와 메서드에 접근하기
@Test
public void invoke()
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Child child = new Child();
Class clazz = child.getClass();
//필드 접근
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
System.out.println(field.get(child));
//cstr1
//cstr2
}
fields[0].set(child, "cstrNew");
System.out.println(child.cstr1); //cstrNew
//메서드 접근
Method method = clazz.getDeclaredMethod("method4", int.class);
method.invoke(child,4); //method4: 4
Method priMethod = clazz.getDeclaredMethod("method5", int.class);
priMethod.setAccessible(true);
priMethod.invoke(child, 5); //method5: 5
}
private 접근자의 경우 setAccessible()의 인자를 true로 넘겨주어야 한다.
장단점
장점
- 런타임 시점에서 클래스의 인스턴스를 생성하고, 접근 제어자와 관계없이 필드와 메서드에 접근하여 필요한 작업을 수행할 수 있는 유연성을 가지고 있다.
단점
- 캡슐화를 저해한다.
- 런타임 시점에서 인스턴스를 생성하므로 컴파일 시점에서 해당 타입을 체크할 수 없다.
- 런타임 시점에서 인스턴스를 생성하므로 구체적인 동작 흐름을 파악하기 어렵다.
- 단순히 필드 및 메서드를 접근할 때보다 리플렉션을 사용하여 접근할 때 성능이 느리다. (모든 상황에서 성능이 느리지는 않음.)
사용하는 이유
리플렉션 API를 통해 런타임 중, 클래스 정보에 접근하여 클래스를 원하는 대로 조작할 수 있다. 심지어 private 접근 제어자로 선언한 필드나 메서드까지 조작이 가능하다.
규모가 작은 콘솔 단계에서는 개발자가 충분히 컴파일 시점에 프로그램에서 사용될 객체와 의존 관계를 모두 파악할 수 있다. 하지만 프레임워크와 같이 큰 규모의 개발 단계에서는 수많은 객체와 의존 관계를 파악하기 어렵다. 이때 리플렉션을 사용하면 동적으로 클래스를 만들어서 의존 관계를 맺어줄 수 있다.
가령, Spring의 Bean Factory를 보면, @Controller, @Service, @Repository 등의 어노테이션만 붙이면 Bean Factory에서 알아서 해당 어노테이션이 붙은 클래스를 생성하고 관리해 주는 것을 알 수 있다. 개발자는 Bean Factory에 해당 클래스를 알려준 적이 없는데, 이것이 가능한 이유는 바로 리플렉션 덕분이다. 런타임에 해당 어노테이션이 붙은 클래스를 탐색하고 발견한다면, 리플렉션을 통해 해당 클래스의 인스턴스를 생성하고 필요한 필드를 주입하여 Bean Factory에 저장하는 식으로 사용이 된다.
물론, 위에서 말했듯 캡슐화를 저해하므로 꼭 필요한 상황에서만 사용하는 것이 좋다.
'JAVA' 카테고리의 다른 글
[JAVA] 체스를 구현하면서 배운점 (0) | 2023.07.06 |
---|---|
[JAVA] 블랙잭을 구현하면서 배운 점 (0) | 2023.06.20 |
[JAVA] 불변 객체를 사용해야 하는 이유 (2) | 2023.05.17 |
다양한 가비지 컬렉션(Garbage Collection) 알고리즘 (0) | 2023.05.15 |
[JAVA] Private 메서드 테스트 방법 및 지양하는 이유 (0) | 2023.05.15 |