Reflection + Annotation
- SpringBoot의 Reflection 문서 참고 (나무에 깃발 꽂기)

심화 : 동적 매개변수 추가
- 리플렉션으로 메서드 동적 실행 시 매개변수도 동적으로 할당하는 방법
매개변수를 동적으로 전달하는 방법
- Method 클래스의 invoke()를 사용할 때, 매개변수를 object[] 배열로 저장하여 반환
- invoke(obj, parameters) 형태로 invoke 시 해당 메서드에 매개변수 전달 가능
Q1. 메서드 오버로딩 등으로 매개변수 개수가 달라질 경우에는?
- Method 클래스의 getParameterCount() 메서드를 활용하여 매개변수의 개수 확인 가능
- getParameterTypes() 메서드를 통해 매개변수 타입도 확인 가능
- 단, Method 클래스에서 매개변수를 가져올 때 이름은 제거하므로 매개변수 명은 확인 불가
Q2. 특정 메서드만 찾고 싶다면?
- Method 클래스의 getDeclaredMethods()를 이용한 선언된 메서드 배열의 getName이 내가 찾는 메서드의 이름과 똑같은 메서드를 찾고 실행
Method target = Arrays.stream(clazz.getDeclaredMethods())
.filter(m -> m.getName().equals("greet"))
.findFirst().orElseThrow();
int paramCount = target.getParameterCount();
System.out.println("greet() needs " + paramCount + " params.");
Q3. 매개변수 타입이 아니라 이름으로 일치 시켜서 찾을 수 있는가?
- java 내에서 바로 지원하지는 않고, 설정을 추가해야 함
- javac 내부에 compilerArgs 옵션에
-parameter
옵션을 추가해서 이름 매핑 가능
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
Ex. Reflection + Annotation에 파라미터 할당
순서
- 내가 찾을 컨트롤러의 메서드를 전부 불러옴
- 내가 불러올 메서드들이 해당되어 있는
@RequestMapping
어노테이션을 전부 찾음
- 메서드에 선언된 매개변수를 불러옴
- 매개변수를 담아둘 배열, List, Map 등의 Collector 선언
- 매개변수 바인딩
- 매개변수의 어노테이션 클래스가
Principal.class
이면 SessionUser.getInstance()로 매핑 - Singleton 패턴
- 매개변수 타입이
Model.class
이면 Model.getInstance()로 매핑 - 역시 Singleton 패턴
- 다 아닐 경우 직접 매핑 필요
- 자바의 기본 설정으로는 매개변수 타입으로만 구분이 가능
- 타입이 다르면 값을 바인딩할 수 있지만, 타입이 전부 동일할 경우 구분할 수 없음
- 매개변수 이름을 저장하지 않기 때문
- 이를 해결하려면 이름 바인딩이 무조건 필요
- javac 수정을 하거나 커스텀 어노테이션 필요
- SpringBoot 등의 서버에서 사용하는 어노테이션은 이름 바인딩 지원
- 내가 입력한 path와 어노테이션에 적은 path = rm.value가 동일하면 method를 invoke
- 이때 UserController의 새 객체와 매개변수를 함께 넘겨야 함
- method.invoke(Controller 객체, params) 의 형태로 선언
코드
- App.java
package ex03;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
// Made by developer A
public class App {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String path = sc.nextLine();
Method[] methods = UserController.class.getDeclaredMethods();
UserController uc = new UserController();
for (Method method : methods) {
Annotation anno = method.getDeclaredAnnotation(RequestMapping.class);
RequestMapping rm = (RequestMapping) anno;
Parameter[] params = method.getParameters();
List<Object> paramList = new ArrayList<>();
for (int i = 0; i < params.length; i++) {
if(params[i].isAnnotationPresent(Principal.class)) paramList.add(SessionUser.getInstance());
// Principal이 없으면 Model의 instance를 저장함
// 문제가 되는 이유 : @Principal이 없으면 Model 또는 다른 매개변수들이 들어가야 함
// Q1. Model 또는 아닌 걸로 구분이 가능한가? : Yes, getClass == Model.class 조건문으로 체크 가능함
else if(params[i].getType().equals(Model.class)) paramList.add(Model.getInstance());
// Q2. 매개변수 타입이 Model.class가 아니면 값을 바인딩할 수는 있는가? : Yes, 해당 클래스에 맞는 값을 바인딩하면 됨
// Q3. 매개변수 타입이 다 동일할 때에도 구분이 가능한가? : No, 매개변수를 타입으로만 구분 가능하기 때문에 전부다 int 또는 String이면 구분 불가 : 이름 바인딩이 필요
// 이름 바인딩은 SpringBoot 등의 서버에서 이미 구현 및 사용되고 있음
// 직접 사용하려면 커스텀 어노테이션 추가 또는 javac 설정 파일 수정 필요
else {
Class<?> type = params[i].getType();
if (type == String.class) {
paramList.add("dummy");
} else if (type == int.class || type == Integer.class) {
paramList.add(0);
} else if (type == double.class || type == Double.class) {
paramList.add(0.0);
} else if (type == boolean.class || type == Boolean.class) {
paramList.add(false);
} else {
paramList.add(null); // 매핑할 수 없는 타입은 null
}
}
}
if (rm.value().equals(path)) {
try {
method.invoke(uc, paramList.toArray()); // model 또는 sessionUser 맞춰서 넣기,
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
}
}
- UserController.java
package ex03;
// Made by developer B
public class UserController {
@RequestMapping("/userinfo")
// 매개변수 @Principal 붙어있으면 땡겨오기
public void userinfo(@Principal SessionUser sessionUser) {
System.out.println(sessionUser.getId());
System.out.println(sessionUser.getUsername());
System.out.println("userinfo call");
}
@RequestMapping("/login")
// model instance 값 땡겨오기
public void login(Model model) {
System.out.println(model.getAttribute("username"));
System.out.println("login call");
}
@RequestMapping("/join")
public void join(String username, String password, String email) {
System.out.println("Registered User : " + username);
System.out.println("pwd length : " + password.length());
System.out.println("Password : " + "*".repeat(password.length()));
System.out.println("Email : " + email);
System.out.println("join call");
}
// 로그아웃이 필요!!
@RequestMapping("/logout")
public void logout() {
System.out.println("logout call");
}
}
- RequestMapping.java
package ex03;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequestMapping {
String value();
}
- Principal.java
package ex03;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Principal {
}
- Model.java
package ex03;
import java.util.HashMap;
import java.util.Map;
public class Model {
private static Model instance = new Model();
public Model() {
attributes.put("username", "ssar");
}
public static Model getInstance() {
return instance;
}
private Map<String, Object> attributes = new HashMap<String, Object>();
public void addAttribute(String name, Object value) {
attributes.put(name, value);
}
public Object getAttribute(String name) {
return attributes.get(name);
}
}
- SessionUser.java
package ex03;
public class SessionUser {
private int id;
private String username;
private static SessionUser instance = new SessionUser();
private SessionUser() {
this.id = 1;
this.username = "ssar";
}
public static SessionUser getInstance() {
return instance;
}
public int getId() {
return id;
}
public String getUsername() {
return username;
}
}
Result
- SessionUser와 Model은 정상적으로 바인딩이 되었음
- 우려한 대로 매개변수가 모두 String인 join을 호출하면 매개변수 값이 전부 더미인 “dummy”로 선언되기 때문에 username, password, email이 전부 dummy로 들어감

Share article