본문 바로가기
Back-end/Spring Boot

[SpringBoot] 스프링부트 이해하기 - 6편 리플렉션과 어노테이션으로 디스패처 필터 만들어보기

by whatamigonnabe 2022. 7. 21.

이번 시간에는 지난번에 살펴본 리플렉션과 어노테이션을 활용하여 디스패처 필터를 직접 만들어보고자합니다. 간단하게 만들어보면서 스프링에서 제공하는 디스패처 서블릿의 일부를 이해할 수 있을 것 같습니다.

이번엔 '메타코딩'님의 유튜브 자료를 참고했습니다.

https://www.youtube.com/watch?v=P5fPc2tjOko&list=PL93mKxaRDidFGJu8IWsAAe0O7y6Yw9f5x 

컨트롤러와 DTO 구현하기

디스페처를 통해 최종적으로 동작시킬 컨트롤러와 DTO를 우선 구현합니다.

참고로 리플렉션을 배우면서 하나 더 알게 된 DTO를 사용하는 이유가 있습니다. 호출한 메서드 마다 필요한 값(필드, key)가 다 달라서, 어떤 메서드가 호출될지 모르기 때문에 어려운 점이 있지만, 메서드마다 필요한 필드들로만 만든 DTO를 사용하게 되면 리플렉션을 통해서 정확히 필요한 값을 파악할 수 있고 Validation check하기에도 편리합니다.

public class UserController {

	public String join(JoinDto joinDto) {
		System.out.println("calling join()");
		System.out.println(joinDto);
		return "/"; // 호출할 path 반환
	}
	
	public String login(LoginDto loginDto) {
		System.out.println("calling login()");
		System.out.println(loginDto);
		return "/";
	}
}
//JoinDto.java
public class JoinDto { //세 개의 값을 받습니다.
	private String userID;
	private String userPassword;
	private String userEmail;
    
    //getters and setters
    //override toString
}
//LoginDto.java
public class LoginDto { //로그인은 아이디와 패스워드 두 개만 받습니다.
	private String userID;
	private String userPassword;
    
    //getters and setters
    //override toString
}

어노테이션 구현하기

어노테이션이 없어도 같은 기능을 구현할 수는 있지만, 어노테이션을 사용함으로서 읽은 사람이 쉽게 내부 동작과정을 유추할 수 있어 가독성을 높일 수 있습니다.

@Retention(RetentionPolicy.RUNTIME) //런타임까지 유지
@Target(ElementType.METHOD) //메소드에만 붙일 수 있음
public @interface RequestMapping {
	String value(); //매핑할 URI 엔드포인트를 담는 엘리먼트
}

만들어두었던 컨트롤러에 어노테이션을 붙입니다.

@RequestMapping("/join") //이 엔드포인트로 접근하면 join()을 호출한다는 내부 동작을 쉽게 유추 가능
public String join(JoinDto joinDto) {
...(중략)
@RequestMapping("/login")
public String login(LoginDto loginDto) {
...(중략)

 

web.xml 에서 필터 지정하기

우선 'web.xml'은 아파치 등의 웹서버가 web.xml의 파일을 읽어, 이곳에 정의해놓은 것에 따라 클라이언트의 요청을 서블릿이나 필터 등으로 매핑하는 역할을 합니다. 또한 필터는 'request/response'와 웹 자원 사이에서 특정한 작업을 수행하는 클래스입니다. 클라이언트의 요청이 들어오면 WAS(Web Application Server, ex. 톰캣)이 web.xml을 읽고 필터가 있으면 필터를 먼저 생성 후 호출하여, client의 httpRequest를 httpHttpServletRequest 객체로 변환하여 넘겨줍니다. 

우선 web.xml에 클라이언트의 모든 요청을 앞으로 작성할 디스패처 필터를 통하도록 설정을 하겠습니다.

//web.xml
<web-app version="4.0" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee                       http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd">
	<filter>
		<filter-name>dispatcher</filter-name>
		<filter-class>com.cos.reflect.filter.Dispatcher</filter-class> //곧 구현할 디스패처 클래스
	</filter>
	
	<filter-mapping>
		<filter-name>dispatcher</filter-name>
		<url-pattern>/*</url-pattern> //모든 uri는 먼저 이 필터에 매핑됨
	</filter-mapping>
</web-app>

디스패처 필터 구현하기

public class Dispatcher implements Filter{ //Filter를 상속받아야함

	@Override
	public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
			throws IOException, ServletException {
		//클라이언트가 요청한 URI path를 파싱하기 위한 메서드들을 사용하기 위해 HttpServletRequest로 다운캐스팅
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) resp;
		
		//엔드포인트 파싱하기
		String endPoint = request.getRequestURI().replaceAll(request.getContextPath(), "");
		System.out.println("Client requesting endpoint: " + endPoint);
		
		//컨트롤러의 서비스 호출 메서드들 런타임시에 리플렉션을 통해 얻기
		UserController userController = new UserController();
		System.out.println("creating a controller object");
		Method[] methods = userController.getClass().getDeclaredMethods();
		
		//어노테이션으로 디스패치하기 
		//리플렉션으로 얻은 컨트롤러의 호출 메서드들을 돌면서, 실제로 클라이언트가 요청한 uri가 무엇인지 확인
		for(Method method: methods) {
			System.out.println("method: " + method.getName());
			//어노테이션이 하나밖에 없기 때문에 getDeclaredAnnotion"s로 받지 않음.
			Annotation annotation = method.getDeclaredAnnotation(RequestMapping.class);
			System.out.println("atteched annotion: " + annotation.toString());
			//내가 만든 RequestMapping으로 다운 캐스팅함. 이래야 어노테이션의 엘리먼트를 조회할 수 있음.
			RequestMapping requestMapping = (RequestMapping) annotation;
			//어떤 메서드를 호출할 것인지 파악
	
			if(endPoint.equals(requestMapping.value())) {
				System.out.println("right method found!");
				//호출할 메서드를 파악했다면, 어떤 인자를 넘겨줄 것인지를 파악해야함.
				//그런데 메서드에 따라 받는 매개변수가 다름.
				//그래서 호출된 메서드의 파라미터 타입을 리플렉션으로 파악하여, dto 객체를 인스턴스화 하고,
				//클라이언트로부터 넘겨받은 key값을 통해서 리플렉션으로 setter메서드를 호출하여, 완성된 dto 객체를 넘겨줄 수 있다.
				try {
					//찾은 메서드를 바탕으로 dto객체 만들기
					Parameter[] params = method.getParameters();
					String path = null;
					if(params.length != 0) {
					
						//메서드 매개변수의 타입을 리플렉션으로 찾아 이것의 객체 생성
						Object dtoInstance = params[0].getType().getDeclaredConstructor().newInstance();
						//객체에 리퀘스트로 받은 값들 넣기
						setData(dtoInstance, request);
						//메소드에 dto인자로 넘겨주면서 실행후 리턴 값 받기
						path = (String) method.invoke(userController, dtoInstance);
					
					} else {
						//매개변수 없으면 그냥 실행
						path = (String) method.invoke(userController);
					}
					System.out.println("path: "+ path);
					RequestDispatcher dis = request.getRequestDispatcher(path);
					dis.forward(request, response);
					break;
				} catch (Exception e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			
			
		}
		
	}

	private void setData(Object dtoInstance, HttpServletRequest request) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		// TODO Auto-generated method stub
		Enumeration<String> keyParams = request.getParameterNames();
		
		while(keyParams.hasMoreElements()) {
			String key = (String) keyParams.nextElement();
			String methodKey = keyToMethod(key);
			Method[] methods = dtoInstance.getClass().getDeclaredMethods();
			for(Method method: methods) {
				if(method.getName().equals(methodKey)) {
					method.invoke(dtoInstance, request.getParameter(key));
					break;
				}
			}
		}
		
	}
	
	private String keyToMethod(String key) {
		return "set" + key.substring(0, 1).toUpperCase() + key.substring(1);
	}
	

}