우아한테크코스 Level4에서 Spring MVC를 만드는 미션에서 Reflection을 사용했습니다.
따라서, Reflection에 대해 조금 더 알아보았습니다.
oracle 문서에서는 Reflection에 대해 다음과 같이 설명합니다.
Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions.
Reflection은 보안 제한 내에서, 로드된 클래스들의 필드, 메서드, 생성자에 관한 정보를 알게해주고 이들을 사용한 opreration을 가능하게 합니다.
출처 : https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/index.html
여기서 말하는 security restrictions(보안 제한)이라는 것이 어색하게 다가왔는데요. 제 생각에는 클래스 타입에 대한 제한적인 정보 정도로 해석할 수 있다고 생각합니다.
즉, 클래스 타입을 몰라도 해당 클래스의 정보(필드, 메서드, 생성자)에 대한 정보를 알 수 있고 이들을 통한 작업을 가능하게 해주는 것이 Reflection 이라고 생각합니다.
조금 설명을 덧붙히자면, ClassLoader는 byte code를 읽어 JVM의 메모리에 클래스들의 정보를 저장합니다. 여기서 우리가 사용하고 싶은 Class는 실제 대상이고, JVM의 메모리에 저장된 정보가 실제 대상의 반사체(Reflection)입니다.
따라서, 해당 클래스 타입에 대한 정보에 접근을 하고 이를 통해 추가적인 operation을 할 수 있는 것입니다.
사실, 서비스 개발을 하는 과정에서는 Reflection을 사용할 일이 크게 없는 것으로 알고 있습니다. Reflection은 Framework 혹은 Library 개발에 많이 사용됩니다. 단편적인 예로, Spring framework는 저희가 정의하는 모든 타입의 Controller를 동작가능하게 합니다. @Controller 어노테이션만 붙어있다면 말이죠. @Controller 어노테이션이 존재하는 메서드에 대해 특정 동작을 하도록 하는 것이죠. 이렇게 구체적인 타입을 알지 못하는 상황에서 Reflection이 사용됩니다.
JVM의 메모리에 적재된 Class의 정보는 Class 객체를 통해 얻을 수 있습니다.
Instances of the class Class represent classes and interfaces in a running Java application.
Class has no public constructor. Instead a Class object is constructed automatically by the Java Virtual Machine when a class loader invokes one of the defineClass methods and passes the bytes of a class file.
Class 객체는 실행되고 있는 Java Application의 class들과 interface들을 나타냅니다.
Class는 public 생성자가 없습니다. 이는 class loader가 정의된 클래스를 로딩하고 class file을 passing할 때(JVM 메모리에 올릴 때 정도로 해석) JVM에 의해 자동으로 생성됩니다.
Application이 생성될 때, 클래스들의 정보를 담은 Class 객체들이 생성되는 것입니다.
아래와 Class 객체에 같이 접근할 수 있습니다.
public class Student {
private String name;
private int age;
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
위와 같은 Student 객체에 대해 아래와 같은 방식으로 Class 정보를 얻을 수 있습니다.
// 방법 1
Class<?> clazz = Class.forName("Student");
// 방법 2
Class<Student> clazz = Student.class;
해당 Class 객체의 메서드들을 통해 필드, 메서드, 생성자에 대한 정보를 얻을 수 있는데요.
어떻게 얻는지 하나씩 살펴보겠습니다.
위의 메서드를 통해 특정 클래스의 Field에 대한 정보를 얻을 수 있습니다.
메서드 이름에 Declared가 있는 것이 있고 없는 것이 있는데요 어떤 차이일까요?
Returns an array containing Field objects reflecting all the accessible public fields of the class or interface represented by this Class object.
Class에 존재하는 모든 public 필드를 반환합니다. 상속받은 public 필드가 존재한다면 이 역시도 반환합니다.
Returns an array of Field objects reflecting all the fields declared by the class or interface represented by this Class object. This includes public, protected, default (package) access, and private fields, but excludes inherited fields.
상속 받은 필드는 포함하지 않고, 클래스에 존재하는 모든 접근제어의 필드를 반환합니다.
아래에서 Setter가 없는 Student 객체의 값을 바꿔보겠습니다.
Class<Student> clazz = Student.class;
Student object = new Student();
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(object, "chris");
System.out.println(object.getName()); // 출력 값 : chris
https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Method.html
위에서 getMethods와 getDeclaredMethods의 차이는 아래와 같습니다.
Returns an array containing Method objects reflecting all the public methods of the class or interface represented by this Class object, including those declared by the class or interface and those inherited from superclasses and superinterfaces.
class 혹은 interface에 정의된 메서드와 상위 class와 상위 interface에 정의된 것을 포함한 모든 public 메서드들을 반환합니다. 상속받은 public 메서드들과 해당 객체에 선언된 public 메서드들을 반환하는 것입니다.
Returns an array containing Method objects reflecting all the declared methods of the class or interface represented by this Class object, including public, protected, default (package) access, and private methods, but excluding inherited methods.
getDeclaredFields와 마찬가지로, 상속 받은 메서드는 포함하지 않고 클래스에 존재하는 모든 접근 제어의 메서드를 반환합니다.
public class Junit4Test {
@MyTest
public void one() throws Exception {
System.out.println("Running Test1");
}
@MyTest
public void two() throws Exception {
System.out.println("Running Test2");
}
public void testThree() throws Exception {
System.out.println("Running Test3");
}
}
class Junit4TestRunner {
@Test
void myTest1() throws Exception {
Class<Junit4Test> clazz = Junit4Test.class;
Junit4Test object = new Junit4Test();
Method method = clazz.getDeclaredMethod("one");
method.invoke(object);
}
@Test
void myTest2() throws Exception {
Class<Junit4Test> clazz = Junit4Test.class;
Junit4Test object = new Junit4Test();
Method[] methods = clazz.getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(MyTest.class)) {
method.invoke(object);
}
}
}
}
myTest1처럼 이름을 통해 Method를 얻어, 실행할 수 있고
myTest2처럼 Annotation을 가지고 있는지 확인할 수도 있습니다.
Junit의 경우에 @Test가 있는 테스트만 실행하는데, 위처럼 Reflection을 통해서 구현한 것입니다.
https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Constructor.html
Returns an array containing Constructor objects reflecting all the public constructors of the class represented by this Class object.
class의 public 생성자들을 반환합니다. Method와 Field와 다르게 상위 객체의 생성자는 반환하지 않습니다.
Returns an array of Constructor objects reflecting all the constructors declared by the class represented by this Class object. These are public, protected, default (package) access, and private constructors.
class의 모든 접근 제어 생성자들을 반환합니다.
public class Student {
private String name;
private int age;
public Student(String name) {
this.name = name;
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
Class<Student> clazz = Student.class;
Constructor<Student> declaredConstructor = clazz.getDeclaredConstructor(String.class);
Student student = declaredConstructor.newInstance("chris");
System.out.println(student.getName()); //출력값 : chris
Class<Student> clazz = Student.class;
Constructor<Student> declaredConstructor = clazz.getDeclaredConstructor(String.class, int.class);
Student student = declaredConstructor.newInstance("chris", 10);
System.out.println(student.getName()); // 출력값 : chris
System.out.println(student.getAge()); // 출력값 : 10
위 처럼, getDeclaredConstructor의 매개변수에 파라미터의 타입들을 넣으면 그에 맞는 생성자를 반환합니다.
Field, Method, Constuctor를 얻는 Class의 메서드는 직접 사용해보면 어렵지 않게 사용하실 수 있을 것입니다.
effective java 아이템 65를 보면 아래와 같은 Reflection의 단점을 말하고 있습니다.
리플렉션을 통한 메서드 호출은 일반 메서드 호출보다 훨씬 느리다고 합니다.
getMethod(String methodName)과 같은 메서드를 사용했을 시에, 해당 methodName을 가진 method가 존재하지 않을 수도 있고, 반환 타입이 다를 수도 있습니다. 이렇게 컴파일타임의 이점을 누릴 수가 없습니다.
위의 사용 예제에서 나타나듯이, 코드가 상당히 복잡해집니다.
Spring MVC를 만들어보는 학습에서 Reflection을 사용했고 사용하면서 학습을 해보았습니다.
사실 Service를 개발하면서 Reflection을 사용해본 경험도 없고, 앞으로 쓸 일이 크게 있을까 싶습니다.
Reflection은 유연함을 가져다 줄 수 있지만, 그에 따른 트레이드오프도 크다고 생각합니다. 따라서, 사용해야 할 상황이 생겨도 신중이 사용해야 할 것 같습니다!