스프링/리플렉션

스프링부트 - 리플렉션 03 리플렉션 메서드 심층 분석

H-V 2021. 9. 12. 10:55

유투버 '데어프로그래밍'님 강의 참조

 

 

1) 파라미터를 받아보자!

  • 파라미터를 위한 모델 User
public class User {
	private int id;
	private String username;
	private String password;
	private String email;
	
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getUsername() {
		return username;
	}
	public void setUsername(String username) {
		this.username = username;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	public String getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
	@Override
	public String toString() {
		return "User [id=" + id + ", username=" + username + ", password=" + password + ", email=" + email + "]";
	}
}

 

 

 

2) 컨트롤러에 파라미터 설정

※ 통신을 위한 데이터는 DTO를 꼭 쓰는게 좋다! 디비와 연결해서 쓰는 데이터는 모델링을 해서 쓰면 되지만 단순 통신상에서는 모델에서 필요없거나 받지 못하는 데이터(회원가입시 ID값/로그인시 EMAIL값 등등)가 NULL값이 되는 경우가 있기 때문이다. 즉 필요한 데이터만 왔다갔다해야 유효성검사에서 이점이 많다

 

  • 각 기능의 DTO 설정

public class JoinDto {
	private String username;
	private String password;
	private String email;
	
	public String getUsername() {
		return username;
	}
	public void setUsername(String username) {
		this.username = username;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	public String getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
	
	@Override
	public String toString() {
		return "JoinDto [username=" + username + ", password=" + password + ", email=" + email + "]";
	}
}

LoginDto 로직 동일

 

 

3) 파라미터 받기 (UserController)

public class UserController {
	
	@RequestMapping("/user/join")
	public String join(JoinDto dto) {
		System.out.println("join() 함수 호출 됨");
		System.out.println("----------------");
		return "/";
	}
	
	@RequestMapping("/user/login")
	public String login(LoginDto dto) {
		System.out.println("login() 함수 호출 됨");
		System.out.println("----------------");
		return "/";
	}
  • 이렇게 DTO로 값을 필요한 것만 딱 받아야 리플렉션이 쉽게 찾을 뿐만 아니라 개발자 입장에서도 일일이 생각할필요없이 DTO로 필요값만 세팅을 해놓고 그 클래스를 파라미터로 받아야 실수도 낮아지고 효율성도 올라간다

 

  • 디스패처 필터에서 파라미터 받기
  • 디스패처에서 파라미터를 어떻게 정확하게 이름을 맞춰서 받을 수 있을까? 답은 직접 하나씩 정확한 값을 받아서 변수화 시키는 방법뿐! 아래 코드를 보자
public class Dispatcher implements Filter {

	@Override
	public void doFilter(ServletRequest req, ServletResponse resp, FilterChain arg2)
			throws IOException, ServletException {
		
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) resp;
		
		String endPoint = request.getRequestURI().replaceAll(request.getContextPath(), "");
		System.out.println("엔드포인트-> "+endPoint);
		
		UserController userController = new UserController();
		Method[] methods = userController.getClass().getDeclaredMethods();// 그 파일에 메서드만!!
		for (Method method : methods) {
			Annotation annotation = method.getDeclaredAnnotation(RequestMapping.class);
			RequestMapping requestMapping = (RequestMapping)annotation;
			
			if(requestMapping.value().equals(endPoint)) {
				try {
					Parameter[] params = method.getParameters();
					String path = null;
					if(params.length != 0) {
						Object dtoInstance = params[0].getType().newInstance();
						setData(dtoInstance, request);
						path="/";
					} else {
						path = (String)method.invoke(userController);
					}
					RequestDispatcher dis = request.getRequestDispatcher(path);
					dis.forward(request, response);
				} catch (Exception e) {
					e.printStackTrace();
				}
				break;
			}
		}
	}
	private <T> void setData(T instance, HttpServletRequest request) {
		Enumeration<String> keys = request.getParameterNames(); //크기 2
		while (keys.hasMoreElements()) { //2번 돔
			String key = (String) keys.nextElement();
			String methodKey = "setUsername";
		}
	}
}

* 프로세스를 이해할 필요가 있다

  1. 주소를 받는다 → endPoint 만듬 (/user/login)
  2. 컨트롤러 클래스를 만든 후 클래스 안에 있는 모든 메소드를 담는다 
    더보기
    UserController userController = new UserController();
    Method[] methods = userController.getClass().getDeclaredMethods();// 그 파일에 메서드만!!
  3. 포문을 돌면서 어노테이션이 있는만큼 돌고 달려있는 어노테이션을 분석해서 'annotation'을 선언한다. 
  4. 'annotation' 에 담긴 메소드를 다운캐스팅해서 requestMapping 변수를 만들면 RequestMapping 인터페이스에서 선언된 value()를 불러 올 수 있다. 즉 endPoint 값이 컨트롤러 어노테이션 값이랑 비교를 할 수 있다는 말
  5. 예로 첫번째 함수가 Join()함수로 이 함수의 @RequestMapping 된 '/user/join' 이 endpoint와 equal인지 비교 한 후 맞으면 try를 탄다
    더보기
    Method[] methods = userController.getClass().getDeclaredMethods();// 그 파일에 메서드만!!
    for (Method method : methods) {
    Annotation annotation = method.getDeclaredAnnotation(RequestMapping.class);
    RequestMapping requestMapping = (RequestMapping)annotation;

    if(requestMapping.value().equals(endPoint)) {
  6.  try에서 params에 각 메소드의 파라미터를 담고 길이가 'params'의 길이가 당연히 0이 아니니 리스트 형의 'params'에서 0번째로 들어온 파라미터('LoginDto dto')를 Object형으로 변수로 만든다. Object형으로 받는 이유는 정확하게 어떤 형으로 다운캐스팅을 해야하며 어떤 파라미터가 들어올지 모르기때문에 Object형으로 선언해놓으면 가져다 쓰기 쉽다
    (여러 파라미터면 포문을 돌면서 뽑으면 된다) 
    더보기
    if(params.length != 0) {
    Object dtoInstance = params[0].getType().newInstance();
  7. 이제 선언된 오브젝트형 파라미터 타입을 리플렉션해서 set함수로 바꿔서 호출을 하면 되는 것. 
    이 set함수를 호출시에는 사용자에게 받은 파라미터(username/password)로 근거를해서 호출 하는데 setUsername/setPassword라고 정확하게 만든 후에 비교를 시켜서 리플렉션을 처리하는 방법이다
  8. setData() 를 통해 타입(dtoInstance)과 request로 받은 username/password을 병합해서 정확하게 이름을 만들어야하는데 이것을 해결하는것이 아래 함수
    더보기
    private <T> void setData(T instance, HttpServletRequest request)
  9. 'keys'로 키값을 두번 받는다 (username,password). 그 후 while문을 돌면서 키값을 하나씩 들고 와 key 변수를 선언하고 이 key 변수를 사용해서 methodKey = 'setUsername'을 정확하게 만드는 것이 목적! 이 부분이 최고 어려운 부분 왜냐면 'set', 'U', 'sername'을 하나씩 불러와서 합쳐야 하기 때문

 

 

4) 'setUsername'을 만드는 테스트 (JUnit)

  • Build Path로 Junit 4 를 설정 한 후 패키지 설정

 

public class setNamingTest {
	
	@Test
	public void keyToSetter() {
		String key = "username";
		
		String firstKey = "set";
		String upperKey = key.substring(0,1).toUpperCase();
		String remainKey = key.substring(1);
		
		System.out.println(firstKey);
		System.out.println(upperKey);
		System.out.println(remainKey);
		
		System.out.println();
		
		String resuslt = firstKey+upperKey+remainKey;
		System.out.println(resuslt);
	}
}

굿!

  • 처음은 무조건 'set'
  • 두번째로 들어온 변수값의 0번째 즉 username이면 u가 upper로 바꿔서 추출
  • 세번째로 U를 빼고 sername을 변수화 
  • 합쳐서 리턴

 

 

  • 이제 이 기능을 디스패처 필터에 넣기만 하면 된다.
	private String keyToMethodKey(String key) {
		String firstKey = "set";
		String upperKey = key.substring(0,1).toUpperCase();
		String remainKey = key.substring(1);
		return firstKey+upperKey+remainKey;
	}
  • 넣은 후 setData함수에 추가
	private <T> void setData(T instance, HttpServletRequest request) {
		Enumeration<String> keys = request.getParameterNames(); //크기 
		while (keys.hasMoreElements()) { //2번 돔
			String key = (String) keys.nextElement();
			String methodKey = keyToMethodKey(key);
		}
	}
  • 이제 어떠한 파라미터값이 넘어와도 username은 setUsername, password는 setPassword 등으로 바뀌고 파라미터개수가 많아도 신경쓸꺼없이 이 두 함수가 다 변환처리를 해준다
  • 이제 세터이름이 변환되면 이거를 바탕으로 리프렉션을 해서 요청된 주소와 매칭을 시키면 되는 것
    더보기
    Method[] methods = instance.getClass().getDeclaredMethods();

→ loginDto가 넘어오면 이 변수에 loginDto에 선언된 모든 함수들이 다 담기는 것

 

  • 이제 methodKey가 가진 값과 methods 에 담긴값을 for문을 돌면서 비교해서 서로 같은것을 찾으면 끝!
	private <T> void setData(T instance, HttpServletRequest request) {
		Enumeration<String> keys = request.getParameterNames(); //크기 
		while (keys.hasMoreElements()) { //2번 돔
			String key = (String) keys.nextElement();
			String methodKey = keyToMethodKey(key);
			
			Method[] methods = instance.getClass().getDeclaredMethods();
			for (Method method : methods) {
				if(method.getName().equals(methodKey)) {
					try {
						method.invoke(instance, request.getParameter(key));
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
		}
	}

* 중요포인트

  • T instance는 파라미터 값, request는 요청으로 같이 넘어온 값(username/password etc..)
  • method.invoke(instance, request.getParameter(key)); 로 유효성이 정상이면 맞는 함수를 호출하게 되는 것
  • request.getParameter에 key가 들어가는 이유는 요청하는것과 맞춰야하기때문 
  • 리턴을 instance로 받을 필요없다. 왜? setData(dtoInstance, request)에서 dtoInstance는 레퍼런스 주소로 넘겼기 떄문. (기본자료형이면 받아야 함)

 

 

  • 최종적으로 path라는 변수에 UserController에 dtoInstance가 가진 파라미터값을 같이 넣어서 때리도록 함(즉 이름과 상관없이 UserCotroller에 dtoInstance 가 LoginDto면 이것을 가진 함수를 호출 하게 되는 것!)
public class Dispatcher implements Filter {

	@Override
	public void doFilter(ServletRequest req, ServletResponse resp, FilterChain arg2)
			throws IOException, ServletException {
		
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) resp;
		
		String endPoint = request.getRequestURI().replaceAll(request.getContextPath(), "");
		System.out.println("엔드포인트-> "+endPoint);
		
		UserController userController = new UserController();
		Method[] methods = userController.getClass().getDeclaredMethods();// 그 파일에 메서드만!!
		boolean isMatching = false;
		
		for (Method method : methods) {
			Annotation annotation = method.getDeclaredAnnotation(RequestMapping.class);
			RequestMapping requestMapping = (RequestMapping)annotation;
			if(requestMapping.value().equals(endPoint)) {
				isMatching = true;
				try {
					Parameter[] params = method.getParameters();
					String path = null;
					if(params.length != 0) {
						Object dtoInstance = params[0].getType().newInstance();
						setData(dtoInstance, request);
						path = (String)method.invoke(userController, dtoInstance);
					} else {
						path = (String)method.invoke(userController);
					}
					RequestDispatcher dis = request.getRequestDispatcher(path);
					dis.forward(request, response);
				} catch (Exception e) {
					e.printStackTrace();
				}
				break;
			}
		}
		if (isMatching == false) {
			response.setContentType("text/html; charset=utf-8");
			PrintWriter out = response.getWriter();
			out.println("잘못된 주소 요청. 404 Error!");
			out.flush();
		}
	}
	private <T> void setData(T instance, HttpServletRequest request) {
		Enumeration<String> keys = request.getParameterNames(); //크기 
		while (keys.hasMoreElements()) { //2번 돔
			String key = (String) keys.nextElement();
			String methodKey = keyToMethodKey(key);
			
			Method[] methods = instance.getClass().getDeclaredMethods();
			for (Method method : methods) {
				if(method.getName().equals(methodKey)) {
					try {
						method.invoke(instance, request.getParameter(key));
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
		}
	}
	
	private String keyToMethodKey(String key) {
		String firstKey = "set";
		String upperKey = key.substring(0,1).toUpperCase();
		String remainKey = key.substring(1);
		return firstKey+upperKey+remainKey;
	}
}

굿!

  • POSTMAN으로 값넣고 테스트

굿!

 

 

  • 리플렉션의 가장 큰 장점은 어떠한 함수를 추가해도 다 찾아서 알아서 호출 해준다는 것
	@RequestMapping("/user/list")
	public String list(User user) {
		System.out.println("list() 함수 호출 됨");
		System.out.println(user);
		System.out.println("----------------");
		return "/";
	}

아이디는 인트형으로 처리해서 받으면 해결 된다!