Guide to Java Reflection -Baeldung 를 통해 자바 리플렉션에 대해 학습한 글입니다.
자바의 Reflection은 런타임시에 동적으로 객체를 생성하고 메서드를 호출할 수 있는 방법이다.
Java Reflection API는 다음 임포트문을 통해서 사용할 수 있다.
import java.lang.reflect.*;
어떻게 Java Reflection을 활용할 수 있을까?
예를 들어, 데이터베이스 테이블에 학생 데이터를 저장하는 테이블인 tblstudent_data가 있다고 해보자. tbl 접두사를 붙여 일관성을 유지했다.
그리고 학생 데이터를 담는 객체를 Student 혹은 StudentData로 정의할 수 있다.
이제 CRUD 작업을 수행할 때, Create 작업은 객체 하나만 매개변수로 받도록 하나의 진입점을 만들 수 있다.
Reflection을 사용하면 객체 이름과 필드 이름을 런타임에 가져올 수 있으므로 이를 이용해 객체 필드와 데이터베이스 테이블 필드를 자동으로 매핑하고 필드 값을 적절한 DB 컬럼에 할당할 수 있다.
Spring 프레임워크의 DI, AOP 그리고 Hibernate, Jackson 등에서 Reflection을 사용한다.
Reflection은 개념 자체로 런타임시에 객체에 접근할 수 있다는 장점이 있으나, 런타임시에 동작하므로 성능 오버헤드가 존재한다는 치명적인 단점이 있다.
또한 CheckedException 발생으로 인한 예외 처리를 해주어야 하고, private 필드 접근 등으로 객체의 은닉셩이 깨져 캡슐화가 위반될 수 있다.
Java Reflection API에서 어떤 기능들을 제공하는지 알아보자.
모든 소스 코드는 Guide to Java Reflection -Baeldung 를 참고하였다.
👉🏻 전체 소스 코드
기본적으로 프로젝트 구조는 다음과 같다.
public class Person {
private String name;
private int age;
}
class PersonTest {
@Test
public void givenObject_whenGetsFieldNamesAtRuntime_thenCorrect() {
Object person = new Person();
Field[] fields = person.getClass().getDeclaredFields();
List<String> actualFieldNames = getFieldNames(fields);
assertTrue(Arrays.asList("name", "age")
.containsAll(actualFieldNames));
}
private static List<String> getFieldNames(Field[] fields) {
List<String> fieldNames = new ArrayList<>();
for (Field field : fields)
fieldNames.add(field.getName());
return fieldNames;
}
}
getSimpleName()는 선언에 나타난 객체의 기본 이름 반환하고, 나머지 두 메서드는 패키지 선언을 포함한 정규화된 클래스 이름을 반환한다.
@Test
public void givenObject_whenGetsClassName_thenCorrect() {
Object goat = new Goat("Goat");
Class<?> clazz = goat.getClass();
assertEquals("Goat", clazz.getSimpleName());
assertEquals("ch9.Goat", clazz.getName());
assertEquals("ch9.Goat", clazz.getCanonicalName());
}
패키지 정보가 포함되어야 한다. 그렇지 않으면 ClassNotException이 발생한다.
@Test
public void givenClassName_whenCreatesObject_thenCorrect() throws ClassNotFoundException{
Class<?> clazz = Class.forName("ch9.Goat");
assertEquals("Goat", clazz.getSimpleName());
assertEquals("ch9.Goat", clazz.getName());
assertEquals("ch9.Goat", clazz.getCanonicalName());
}
Modifier.isPublic(mods), Modifier.isPrivate(mods), Modifier.isFinal(mods) @Test
public void givenClass_whenRecognisesModifiers_thenCorrect() throws ClassNotFoundException{
Class<?> goatClass = Class.forName("ch9.Goat");
Class<?> animalClass = Class.forName("ch9.Animal");
int goatMods = goatClass.getModifiers();
int animalMods = animalClass.getModifiers();
assertTrue(Modifier.isPublic(goatMods));
assertTrue(Modifier.isAbstract(animalMods));
}
@Test
public void givenClass_whenGetsSuperClass_thenCorrect() {
Goat goat = new Goat("goat");
String str = "any string";
Class<?> goatClass = goat.getClass();
Class<?> goatSuperClass = goatClass.getSuperclass();
assertEquals("Animal", goatSuperClass.getSimpleName());
assertEquals("Object", str.getClass().getSuperclass().getSimpleName()); // String 객체의 슈퍼 클래스
}
@Test
public void givenClass_whenGetsImplementedInterfaces_thenCorrect() throws ClassNotFoundException{
Class<?> goatClass = Class.forName("ch9.Goat");
Class<?> animalClass = Class.forName("ch9.Animal");
Class<?>[] goatInterfaces = goatClass.getInterfaces();
Class<?>[] animalInterfaces = animalClass.getInterfaces();
assertEquals(1, goatInterfaces.length);
assertEquals(1, animalInterfaces.length);
assertEquals("Locomotion", goatInterfaces[0].getSimpleName());
assertEquals("Eating", animalInterfaces[0].getSimpleName());
}
객체의 생성자, 메서드, 필드를 검사할 수 있다.
@Test
public void givenClass_whenGetsConstructor_thenCorrect() throws ClassNotFoundException{
Class<?> goatClass = Class.forName("ch9.Goat");
Constructor<?>[] constructors = goatClass.getConstructors();
assertEquals(1, constructors.length);
assertEquals("ch9.Goat", constructors[0].getName());
}
@Test
public void givenClass_whenGetsFields_thenCorrect() throws ClassNotFoundException{
Class<?> animalClass = Class.forName("ch9.Animal");
Field[] fields = animalClass.getDeclaredFields();
List<String> actualFields = getFieldNames(fields);
assertEquals(2, actualFields.size());
assertTrue(actualFields.containsAll(Arrays.asList("name", "CATEGORY")));
}
@Test
public void givenClass_whenGetsMethods_thenCorrect() throws ClassNotFoundException{
Class<?> animalClass = Class.forName("ch9.Animal");
Method[] methods = animalClass.getDeclaredMethods();
List<String> actualMethods = getMethodNames(methods);
assertEquals(3, actualMethods.size());
assertTrue(actualMethods.containsAll(Arrays.asList("getName",
"setName", "getSound")));
}
private static List<String> getFieldNames(Field[] fields) {
List<String> fieldNames = new ArrayList<>();
for (Field field : fields)
fieldNames.add(field.getName());
return fieldNames;
}
// Method 객체 배열에서 메서드 이름을 검색하는 헬퍼 메서드
private static List<String> getMethodNames(Method[] methods) {
List<String> methodNames = new ArrayList<>();
for (Method method : methods)
methodNames.add(method.getName());
return methodNames;
}
@Test
public void givenClass_whenGetsAllConstructors_thenCorrect() throws ClassNotFoundException {
Class<?> birdClass = Class.forName("ch9.Bird");
Constructor<?>[] constructors = birdClass.getConstructors();
assertEquals(3, constructors.length);
}
/*
선언된 순서대로 생성자의 매개변수 클래스 유형을 전달하여 Bird 클래스의 각 생성자를 검색
- NoSuchMethodException이 발생하고 주어진 순서대로 주어진 매개변수 유형을 갖는 생성자가 존재하지 않으면 테스트가 자동으로 실패하므로 assertion이 필요하지 않다.
*/
@Test
public void givenClass_whenGetsEachConstructorByParamTypes_thenCorrect() throws ClassNotFoundException, NoSuchMethodException {
Class<?> birdClass = Class.forName("ch9.Bird");
Constructor<?> cons1 = birdClass.getConstructor();
Constructor<?> cons2 = birdClass.getConstructor(String.class);
Constructor<?> cons3 = birdClass.getConstructor(String.class, boolean.class);
}
@Test
public void givenClass_whenInstantiatesObjectsAtRuntime_thenCorrect() throws Exception{
Class<?> birdClass = Class.forName("ch9.Bird");
Constructor<?> cons1 = birdClass.getConstructor();
Constructor<?> cons2 = birdClass.getConstructor(String.class);
Constructor<?> cons3 = birdClass.getConstructor(String.class,
boolean.class);
// 생성자 클래스의 newInstance 메서드를 호출하고 필요한 매개변수를 선언된 순서대로 전달하여 클래스 객체를 인스턴스화한다.
Bird bird1 = (Bird) cons1.newInstance();
Bird bird2 = (Bird) cons2.newInstance("Weaver bird");
Bird bird3 = (Bird) cons3.newInstance("dove", true);
assertEquals("bird", bird1.getName());
assertEquals("Weaver bird", bird2.getName());
assertEquals("dove", bird3.getName());
assertFalse(bird1.walks());
assertTrue(bird3.walks());
}
런타임에 필드 값을 가져오고 설정할 수 있다.
/*
getFields()
- 해당 클래스의 접근 가능한 모든 public 필드를 반환
- 클래스와 모든 조상 클래스의 모든 public 필드를 반환
*/
@Test
public void givenClass_whenGetsPublicFields_thenCorrect() throws ClassNotFoundException{
Class<?> birdClass = Class.forName("ch9.Bird");
Field[] fields = birdClass.getFields();
assertEquals(1, fields.length);
assertEquals("CATEGORY", fields[0].getName());
}
// getField() - 단 하나의 Field 객체만 반환(조상 클래스에 선언되었지만 자식 클래스에는 선언되지 않은 private 필드에는 접근 불가)
@Test
public void givenClass_whenGetsPublicFieldByName_thenCorrect() throws ClassNotFoundException, NoSuchFieldException{
Class<?> birdClass = Class.forName("ch9.Bird");
Field field = birdClass.getField("CATEGORY");
assertEquals("CATEGORY", field.getName());
}
// 클래스에 선언된 필드를 검사할 수 있다.
@Test
public void givenClass_whenGetsDeclaredFields_thenCorrect() throws ClassNotFoundException{
Class<?> birdClass = Class.forName("ch9.Bird");
Field[] fields = birdClass.getDeclaredFields();
assertEquals(1, fields.length);
assertEquals("walks", fields[0].getName());
}
// 필드 이름을 알고 있는 경우
// 필드 이름을 잘못 입력하거나 존재하지 않는 필드를 입력하면 NoSuchFieldException 발생
@Test
public void givenClass_whenGetsFieldsByName_thenCorrect() throws ClassNotFoundException, NoSuchFieldException{
Class<?> birdClass = Class.forName("ch9.Bird");
Field field = birdClass.getDeclaredField("walks");
assertEquals("walks", field.getName());
}
@Test
public void givenClassField_whenGetsType_thenCorrect() throws ClassNotFoundException, NoSuchFieldException{
Field field = Class.forName("ch9.Bird")
.getDeclaredField("walks");
Class<?> fieldClass = field.getType();
assertEquals("boolean", fieldClass.getSimpleName());
}
필드값을 가져오려면, 먼저 Field 객체에서 setAccessible 메서드를 호출하여 접근 가능하도록 설정하고 boolean값 true를 전달한다.
public void givenClassField_whenSetsAndGetsValue_thenCorrect() throws Exception{
Class<?> birdClass = Class.forName("ch9.Bird");
Bird bird = (Bird) birdClass.getConstructor().newInstance();
Field field = birdClass.getDeclaredField("walks");
field.setAccessible(true);
assertFalse(field.getBoolean(bird));
assertFalse(bird.walks());
field.set(bird, true);
assertTrue(field.getBoolean(bird));
assertTrue(bird.walks());
}
public static으로 선언하면 해당 객체를 포함하는 클래스의 인스턴스가 필요하지 않다. null을 대신 전달해도 필드의 기본값을 얻을 수 있다.
@Test
public void givenClassField_whenGetsAndSetsWithNull_thenCorrect() throws Exception{
Class<?> birdClass = Class.forName("ch9.Bird");
Field field = birdClass.getField("CATEGORY");
field.setAccessible(true);
assertEquals("domestic", field.get(null));
}
@Test
public void givenClass_whenGetsAllPublicMethods_thenCorrect() throws Exception{
Class<?> birdClass = Class.forName("ch9.Bird");
Method[] methods = birdClass.getMethods();
List<String> methodNames = getMethodNames(methods);
assertTrue(methodNames.containsAll(Arrays
.asList("equals", "notifyAll", "hashCode",
"walks", "eats", "toString")));
}
@Test
public void givenClass_whenGetsOnlyDeclaredMethods_thenCorrect() throws ClassNotFoundException{
Class<?> birdClass = Class.forName("ch9.Bird");
List<String> actualMethodNames
= getMethodNames(birdClass.getDeclaredMethods());
List<String> expectedMethodNames = Arrays
.asList("setWalks", "walks", "getSound", "eats");
assertEquals(expectedMethodNames.size(), actualMethodNames.size());
assertTrue(expectedMethodNames.containsAll(actualMethodNames));
assertTrue(actualMethodNames.containsAll(expectedMethodNames));
}
@Test
public void givenMethodName_whenGetsMethod_thenCorrect() throws Exception {
Bird bird = new Bird();
Method walksMethod = bird.getClass().getDeclaredMethod("walks");
Method setWalksMethod = bird.getClass().getDeclaredMethod("setWalks", boolean.class);
assertTrue(walksMethod.canAccess(bird));
assertTrue(setWalksMethod.canAccess(bird));
}
예: Bird클래스의 walks 속성의 기본값은 false -> true로 바꾸기 (setWalks 메서드 호출을 통해)
@Test
public void givenMethod_whenInvokes_thenCorrect() throws Exception{
Class<?> birdClass = Class.forName("ch9.Bird");
Bird bird = (Bird) birdClass.getConstructor().newInstance();
Method setWalksMethod = birdClass.getDeclaredMethod("setWalks", boolean.class);
Method walksMethod = birdClass.getDeclaredMethod("walks");
boolean walks = (boolean) walksMethod.invoke(bird);
assertFalse(walks);
assertFalse(bird.walks());
setWalksMethod.invoke(bird, true);
boolean walks2 = (boolean) walksMethod.invoke(bird);
assertTrue(walks2);
assertTrue(bird.walks());
}