Method - 클래의 메서드에 접근
클래스의 모든 메서드들은 java.lang.reflect.Method타입의 객체로서 표현된다.
Class.getDeclaredMethods()
모든 메서드를 반환값으로 출력한다.
Class.getMethod()
super 클래스에서 상속된 메서드와 인터페이스를 포함한 모든 public 메서드를 출력한다.
Method의 Properties
Method객체의 함수들 중 몇개만 알아보자.
Method.getName(), Method.getSimpleName()
클래스에 사용된 메서드의 이름을 문자열로 추출할 수 있다. (package 명 포함/ 미 포함)
Method.getReturnType()
특정 메서드의 반환 타입을 나타내는 클래스 객체를 알 수 있다.
Method.getParameterTypes(), Method.getParameterCount()
메서드의 매개변수 타입에 대한 정보를 알려준다.
Method.getExceptionTypes()
메서드에 선언된 exception을 배열로 가져온다.
Method.getParameters()
메서드의 매개변수에 대한 정보들을 가져온다.
(매개변수의 이름은 컴파일 과정에서 arg0, arg1과 같이 이름이 대체된다.)
위의 함수들을 활용해서 간단한 getter/ setter 메서드가 존재하는지, 올바른지 체크하는 프로그램을 만들어보자.
해당 프로그램을 만들기 위해 필요한 정보들을 생각해보자.
1. 필드변수들의 정보
2. 메서드의 이름
3. 매개변수의 정보
4. 반환 타입
위 정보들이 올바른지 확인하며 만들어보자.
1. 필드 변수들 가져오기
// 모든 필드 변수 가져오기 - 부모 클래스 포함
private List<Field> getAllFields(Class<?> clazz) {
// null 체크
if (clazz == null || clazz.equals(Object.class)) {
return Collections.emptyList();
}
// 현재 클래스의 필드 변수들
Field[] currentClassFields = clazz.getDeclaredFields();
// 부모 클래스의 필드 변수들
List<Field> superFields = getAllFields(clazz.getSuperclass());
List<Field> allFields = new ArrayList<>();
allFields.addAll(Arrays.asList(currentClassFields));
allFields.addAll(superFields);
return allFields;
}
2. 메서드의 이름 - getter, setter는 public이므로 getMothods() 사용
private Map<String, Method> mapMethodNameToMethod(Class<?> dataClass) {
// public으로 선언된 모든 메서드 - 부모 클래스 포함
Method[] allMethods = dataClass.getMethods();
// 메서드 이름과 메서드 정보 map으로 저장
Map<String, Method> nameToMethod = new HashMap<>();
for (Method method : allMethods) {
nameToMethod.put(method.getName(), method);
}
return nameToMethod;
}
메서드 이름 형식 검사 헬퍼 메서드 - 첫 글자 대문자 만들기
private String capitalizeFirstLetter(String fieldName) {
// 필드 이름 첫 글자 대문자 만들기
return fieldName.substring(0, 1).toUpperCase().concat(fieldName.substring(1));
}
3. 매개변수의 정보 + 4. 반환 타입 검사
(getter)
public void testGetters(Class<?> dataClass) {
// 모든 필드 가져오기
List<Field> fields = getAllFields(dataClass);
// 메서드의 이름, 정보 map으로 가져오기
Map<String, Method> methodNameToMethod = mapMethodNameToMethod(dataClass);
for (Field field : fields) {
// 메서드 이름이 getName과 같은 형식인지
String getterName = "get" + capitalizeFirstLetter(field.getName());
// get + 필드 변수이름 한 메서드가 없으면 에러 발생
if (!methodNameToMethod.containsKey(getterName)) {
throw new IllegalStateException(String.format("Field : %s doesn't have a getter method", field.getName()));
}
// 필드 변수의 getter 메서드 가져오기
Method getter = methodNameToMethod.get(getterName);
// 메서드의 반환타입이 필드 변수의 타입과 일치 하지 않으면 에러 발생
if (!getter.getReturnType().equals(field.getType())) {
throw new IllegalStateException(
String.format("Getter method : %s() has return type %s but expected %s",
getter.getName(),
getter.getReturnType().getTypeName(),
field.getType().getTypeName()));
}
// 메서드에 파라미터가 존재하면 에러 발생
if (getter.getParameterCount() > 0) {
throw new IllegalStateException(String.format("Getter : %s has %d arguments", getterName, getter.getParameterCount()));
}
}
}
(setter)
public void testSetters(Class<?> dataClass) {
// 모든 필드 가져오기
List<Field> fields = getAllFields(dataClass);
for (Field field : fields) {
// 이름 형식 검사
String setterName = "set" + capitalizeFirstLetter(field.getName());
Method setterMethod = null;
try {
// 해당 클래스에 이름 형식에 알맞는 메서드가 없으면 에러 발생
setterMethod = dataClass.getMethod(setterName, field.getType());
} catch (NoSuchMethodException e) {
throw new IllegalStateException(String.format("Setter : %s not found", setterName));
}
// setter 메서드의 반환타입이 void가 아니면 에러 발생
if (!setterMethod.getReturnType().equals(void.class)) {
throw new IllegalStateException(String.format("Setter method : %s has to return void", setterName));
}
}
}
Reflection을 활용한 메서드 실행
Method.invoke(Object instace, Object ...args)
호출하려는 메서드를 가진 인스턴스와 매개변수의 정보를 완전히 일치하게 전달해 해당 메서드를 실행할 수 있다.
호출하려는 메서드가 static이면 instance에 null을 전달해 실행할 수 있다. 기본적으로 반환타입은 Object타입이며, int, float, double, long,... 등 원시타입이면 wrapper 클래스로 감싸서 반환하게 된다. 따라서 메모리를 더 차지하게 된다.
invoke메서드는 여러 에러를 발생시킬 수 있어 조심히 다루어야 하며 특히 InvocationTargetException은 예외처리를 통해 호출한 메서드와 호출 타깃 예외 처리 객체가 구체적인 예외 처리를 감싸면 전달되어 예외 처리를 처리, 로깅, 재전달 할 수 있게 해준다.
invoke메서드를 사용할 때는 메서드 이름, 시그니처, 반환 유형, 메서드를 포함한 클래스가 해당 메서드를 실행하는 로직에서 분리 될 때이다.
실제로 invoke 메서드를 사용해보며 방법과 사용할 때를 익혀보자.
예를 들어, 아래와 같이 외부 라이브러리와 연결을 하는 객체가 있다고 가정해보자. 두 클래스는 완전히 분리된 라이브러리에 속해있다.
public class DataBaseClient {
public boolean storeData(String data) {
System.out.printf("Storing %s in the database%n", data);
return true;
}
}
public class HttpClient {
public boolean sendRequest(String request) {
System.out.println("Sending request: " + request);
return true;
}
}
Reflection의 사용없이 두 객체의 메서드를 사용하고자 한다면 우리는 두 클래스를 인스턴스로 만들고 해당 메서드를 사용하는 식으로 할 것이다. 하지만 이와 매개 변수는 동일하지만 외부 라이브러리로 독립적이며, 이름이 모두 다 다른 객체를 여러개 사용해야할 때 Reflection 없이 우리에게 선택권이 있을까? 단언컨데 없다. 모두 다 인스턴스로 만들고 모두 다 메서드를 호출해야한다. 이와 같은 상황에 처해 있을 때 Reflection을 안다면 우리에겐 선택권이 하나 생기게 된다.
그럼 이제 Reflection을 사용해 동일한 형식의 모든 메서드를 실행할 수 있는 제네릭 함수를 작성해보자.
먼저 객체로부터 실행할 메서드들을 추출하는 메서드를 만들어보자.
이 메서드는 객체와 메서드의 파라미터들을 List로 받는다.
public Map<Object, Method> groupExecutors(List<Object> requestExecutors, List<Class<?>> methodParameters) {
// 인스턴스와 실행할 메서드를 map으로 만듦
Map<Object, Method> instanceMethod = new HashMap<>();
for (Object requestExecutor : requestExecutors) {
// 해당 인스턴스로부터 모든 메서드를 가져오고
Method[] allMethods = requestExecutor.getClass().getDeclaredMethods();
for (Method method : allMethods) {
// 메서드의 파라미터(타입, 순서) 동일 하면 실행할 메서드로 판단
if (Arrays.asList(method.getParameterTypes()).equals(methodParameters)) {
instanceMethod.put(requestExecutor, method);
}
}
}
return instanceMethod;
}
실행할 메서드를 메서드가 있는 인스턴스를 키값으로 한 Map에 담아 어떤 클래스의 메서드인지를 저장한다.
그런 다음 Map을 순차적으로 순회하며 메서드들을 invoke하여 실행한다.
public void executeAll(Map<Object, Method> requestExecutors, String requestBody) throws InvocationTargetException, IllegalAccessException {
// 인스턴스와 실행할 메서드가 담긴 map을 탐색
for (Map.Entry<Object, Method> entry : requestExecutors.entrySet()) {
// 인스턴스 가져오고
Object requestExecutor = entry.getKey();
// 실행할 메서드를 가져와
Method method = entry.getValue();
// 메서드에 인스턴스 정보와 파라미터에 대한 값을 함께 넘겨 실행
method.invoke(requestExecutor, requestBody);
}
}
인스턴스와 메서드에 대한 정보를 담은 Map과 실행시킬 때 전달할 Data를 사용하여 메서드를 실행한다.
위 두 메서드를 사용하여 위에 선언한 두 클래스 메서드를 실행해 보자.
// 데이터
String requestBody = "request Data";
// 파라미터에 타입 - 순서대로
List<Class<?>> methodParameterTypes = Arrays.asList(new Class<?>[]{String.class});
// 클래스에서 사용할 메서드 추출
Map<Object, Method> requestExecutors = groupExecutors(Arrays.asList(new DataBaseClient(), new HttpClient()), methodParameterTypes);
// 메서드 실행
executeAll(requestExecutors, requestBody);
Reflection을 사용하지 않았다면 코드는 아래와 같았을 것이다.
String requestBody = "request Data";
DataBaseClient dataBaseClient = new DataBaseClient();
HttpClient httpClient = new HttpClient();
dataBaseClient.storeData(requestBody);
httpClient.sendRequest(requestBody);
현재는 실행할 메서드가 적어서 오히려 번거롭다고 느껴질 지도 모른다.
Reflection은 단순히 개발자에게 선택권을 늘려주는 것에 불과하며 반드시 사용할 이유도 그래야할 필요도 없으며 항상 왜 사용하는지에 대한 정당한 이유가 있어야한다. Reflection을 사용해 어떠한 로직을 작성한다는 것은 프로그램을 구현하는 여러가지 방법들 중 하나에 불과하니 이러한 방법도 있구나 하면서 보았으면 한다.
'Language > Java' 카테고리의 다른 글
Java native 키워드로 Rust코드와 연동하기 (2) | 2024.09.19 |
---|---|
Java Modifier(제어자) (1) | 2024.09.17 |
Java - unchecked casting (1) | 2024.09.13 |
Java Reflection(2) (0) | 2024.08.25 |
Java Reflection(1) (0) | 2024.08.21 |