이번 시간에는 지난번에 살펴본 리플렉션과 어노테이션을 활용하여 디스패처 필터를 직접 만들어보고자합니다. 간단하게 만들어보면서 스프링에서 제공하는 디스패처 서블릿의 일부를 이해할 수 있을 것 같습니다.
이번엔 '메타코딩'님의 유튜브 자료를 참고했습니다.
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);
}
}
'Back-end > Spring Boot' 카테고리의 다른 글
[SpringBoot] WebClient 이해하기(feat. block/non-block과 동기/비동기) (0) | 2022.08.01 |
---|---|
[SpringBoot] 빌더 어노테이션(@Builder)와 빌더 패턴 (0) | 2022.07.22 |
[SpringBoot] 스프링부트 이해하기 - 5편 Annotation (0) | 2022.07.16 |
[SpringBoot] 스프링부트 이해하기- 4편 Reflection(feat. 동적 로딩) (0) | 2022.07.16 |
[SpringBoot] 스프링부트 이해하기- 3편 IoC와 DI 쉬운 버전 (0) | 2022.07.14 |