[스프링부트] 31. Reflection 심화

문정준's avatar
Jul 24, 2025
[스프링부트] 31. Reflection 심화

Reflection + Annotation

  • SpringBoot의 Reflection 문서 참고 (나무에 깃발 꽂기)
notion image

심화 : 동적 매개변수 추가

  • 리플렉션으로 메서드 동적 실행 시 매개변수도 동적으로 할당하는 방법
 

매개변수를 동적으로 전달하는 방법

  • 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에 파라미터 할당

순서

  1. 내가 찾을 컨트롤러의 메서드를 전부 불러옴
  1. 내가 불러올 메서드들이 해당되어 있는 @RequestMapping 어노테이션을 전부 찾음
  1. 메서드에 선언된 매개변수를 불러옴
  1. 매개변수를 담아둘 배열, List, Map 등의 Collector 선언
  1. 매개변수 바인딩
    1. 매개변수의 어노테이션 클래스가 Principal.class 이면 SessionUser.getInstance()로 매핑
        • Singleton 패턴
    2. 매개변수 타입이 Model.class 이면 Model.getInstance()로 매핑
        • 역시 Singleton 패턴
    3. 다 아닐 경우 직접 매핑 필요
        • 자바의 기본 설정으로는 매개변수 타입으로만 구분이 가능
        • 타입이 다르면 값을 바인딩할 수 있지만, 타입이 전부 동일할 경우 구분할 수 없음
          • 매개변수 이름을 저장하지 않기 때문
        • 이를 해결하려면 이름 바인딩이 무조건 필요
          • javac 수정을 하거나 커스텀 어노테이션 필요
          • SpringBoot 등의 서버에서 사용하는 어노테이션은 이름 바인딩 지원
  1. 내가 입력한 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로 들어감
notion image
 
Share article

sxias