HandlerInterceptor 에 대해 정리 한다.
HandlerInterceptor 는 HandlerMapping 에 적용할 수 있다.
HandlerMapping 은 요청을 처리 할 handler method 를 찾아주는 역할을 한다.
HandlerInterceptor 에 정의한 기능들은 HandlerMapping 이 찾아 준 handler method 에 적용 된다.
실습을 해보기에 앞서 HandlerInterceptor 에 대한 api 문서를 먼저 보자.
문서에 따르면 handler method 의 구현 내용을 수정하지 않고 공통으로 처리 할 작업들을 정의할 수 있다고 되어 있다.
이 인터페이스는 세가지 default method 를 가지고 있는 것으로 보아 굳이 구현하지 않아도 괜찮아 보인다.
package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
import org.springframework.web.method.HandlerMethod;
public interface HandlerInterceptor {
/**
* Intercept the execution of a handler. Called after HandlerMapping determined
* an appropriate handler object, but before HandlerAdapter invokes the handler.
* <p>DispatcherServlet processes a handler in an execution chain, consisting
* of any number of interceptors, with the handler itself at the end.
* With this method, each interceptor can decide to abort the execution chain,
* typically sending a HTTP error or writing a custom response.
* <p><strong>Note:</strong> special considerations apply for asynchronous
* request processing. For more details see
* {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.
* <p>The default implementation returns {@code true}.
* @param request current HTTP request
* @param response current HTTP response
* @param handler chosen handler to execute, for type and/or instance evaluation
* @return {@code true} if the execution chain should proceed with the
* next interceptor or the handler itself. Else, DispatcherServlet assumes
* that this interceptor has already dealt with the response itself.
* @throws Exception in case of errors
*/
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
/**
* Intercept the execution of a handler. Called after HandlerAdapter actually
* invoked the handler, but before the DispatcherServlet renders the view.
* Can expose additional model objects to the view via the given ModelAndView.
* <p>DispatcherServlet processes a handler in an execution chain, consisting
* of any number of interceptors, with the handler itself at the end.
* With this method, each interceptor can post-process an execution,
* getting applied in inverse order of the execution chain.
* <p><strong>Note:</strong> special considerations apply for asynchronous
* request processing. For more details see
* {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.
* <p>The default implementation is empty.
* @param request current HTTP request
* @param response current HTTP response
* @param handler handler (or {@link HandlerMethod}) that started asynchronous
* execution, for type and/or instance examination
* @param modelAndView the {@code ModelAndView} that the handler returned
* (can also be {@code null})
* @throws Exception in case of errors
*/
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
/**
* Callback after completion of request processing, that is, after rendering
* the view. Will be called on any outcome of handler execution, thus allows
* for proper resource cleanup.
* <p>Note: Will only be called if this interceptor's {@code preHandle}
* method has successfully completed and returned {@code true}!
* <p>As with the {@code postHandle} method, the method will be invoked on each
* interceptor in the chain in reverse order, so the first interceptor will be
* the last to be invoked.
* <p><strong>Note:</strong> special considerations apply for asynchronous
* request processing. For more details see
* {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.
* <p>The default implementation is empty.
* @param request current HTTP request
* @param response current HTTP response
* @param handler handler (or {@link HandlerMethod}) that started asynchronous
* execution, for type and/or instance examination
* @param ex exception thrown on handler execution, if any
* @throws Exception in case of errors
*/
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
전체적인 실행 흐름을 다음과 같이 그림을 그려볼 수 있다.
preHandle 에서는 보통 Servlet 에서 사용하던 Filter 와 비슷하지만 보다 구체적인 작업을 할 수 있다.
예를 들어 preHandler 의 메소드 파라미터 중에 Object handler 가 있는 것을 볼 수 있다.
즉, Filter 와 다르게 어떤 class 의 어떤 handler method 가 호출 될 것인지에 대한 정보를 전달 받기 때문에 요청에 대해 보다 구체적인 처리가 가능하다.
preHandle 에서 한가지 더 눈여겨 볼 점은 return type 이 boolean 이라는 것인데, 이 메소드 실행 결과 true 를 반환하면 다음 interceptor 의 preHandler 등 처리를 진행하지만, 그렇지 않고 false 를 반환하면 요청 처리를 더 이상 진행하지 않고 멈춘다.
handler method 까지 실행을 마치고 나서 view 를 렌더링 하기 직전에 postHandle 을 실행 한다.
이 postHandle 은 ModelAndView 타입의 객체를 매개변수로 받는다.
그렇기 때문에 이 곳에서는 model 객체에 추가 정보를 넣거나 view 를 변경하는 등의 작업을 할 수 있다.
afterCompletion 은 veiw 렌더링을 마친 다음으로 모든 요청 처리가 끝난 다음에 호출 된다.
그리고 preHandler 의 반환 값이 true 인 경우 postHandle 실행과 상관 없이 실행 된다.
어떠한 요청 처리 전 후에 필요한 작업을 한다는 점에서 Servlet 의 Filter 와 Spring 의 HandlerInterceptor 는 별다른 차이가 없어 보일 수 있다.
하지만 앞서 정리했던 것처럼 Filter 는 HandlerInterceptor 만큼의 정보를 알고 있지 않다.
그렇다고 해서 무조건 HandlerInterceptor 를 쓰는게 좋은것은 아니다.
만약 전후처리 내용이 Spring 과 밀접한 관련이 있다면 당연히 HandlerInterceptor 를 사용하는게 좋고 또 그래야 하겠지만, 그렇지 않은 성격의 전후처리라면 여전히 Servlet 의 Filter 를 사용하는게 문맥상 더 자연스럽다고 생각한다.
예를 들어 XSS 공격은 특정 handler 에 대한 것도 아니고 Spring 과 무관한 내용이므로 HandlerInterceptor 가 아닌 Servlet Filter 에서 처리하도록 구현 되어야 한다.
그럼 마지막으로 HandlerInterceptor 를 구현해보자.
구현하는 방법은 이 인터페이스를 구현하는 클래스를 만들고 Interceptor 로 등록해주면 된다.
Interceptor 로 GreetingInterceptor 클래스를 만들었다.
package me.nimkoes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class GreetingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("========== GreetingInterceptor preHandle ==========");
System.out.println(">>>>>>>>>> handler");
System.out.println("getClass : " + handler.getClass());
System.out.println("toString : " + handler.toString());
System.out.println();
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("========== GreetingInterceptor postHandle ==========");
System.out.println("isEmpty : " + (modelAndView == null));
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("========== GreetingInterceptor afterCompletion ==========");
}
}
특별한 기능을 넣지는 않고 단순히 테스트 해보기 위해 몇가지 값을 출력하도록 작성 했다.
구현한 HandlerInterceptor 를 등록하기 위해 WebMvcConfigurer 를 구현한 WebConfig 설정 파일에 다음과 같이 Interceptor 를 등록 하였다.
package me.nimkoes;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new GreetingInterceptor());
}
}
그리고나서 미리 작성 되어있던 테스트 코드를 실행해서 결과를 확인해 보았다.
========== GreetingInterceptor preHandle ========== >>>>>>>>>> handler getClass : class org.springframework.web.method.HandlerMethod toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person) ========== GreetingInterceptor postHandle ========== isEmpty : true ========== GreetingInterceptor afterCompletion ========== |
preHandle 이 false 를 반환하도록 고친 다음 실행 해보았다.
========== GreetingInterceptor preHandle ========== >>>>>>>>>> handler getClass : class org.springframework.web.method.HandlerMethod toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person) |
테스트 코드는 깨졌고 로그는 preHandle 까지만 출력 되었다.
GreetingInterceptor 의 preHandle 을 다시 true 를 반환 하도록 고치고 HandlerInterceptor 를 하나 더 추가 했다.
package me.nimkoes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class AnotherInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("========== AnotherInterceptor preHandle ==========");
System.out.println(">>>>>>>>>> handler");
System.out.println("getClass : " + handler.getClass());
System.out.println("toString : " + handler.toString());
System.out.println();
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("========== AnotherInterceptor postHandle ==========");
System.out.println("isEmpty : " + (modelAndView == null));
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("========== AnotherInterceptor afterCompletion ==========");
}
}
새로 만든 AnotherInterceptor 를 포함하여 두 개의 Interceptor 를 등록했다.
package me.nimkoes;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new GreetingInterceptor());
registry.addInterceptor(new AnotherInterceptor());
}
}
실행해보면 재밌는 결과가 출력 된 것을 볼 수 있다.
========== GreetingInterceptor preHandle ========== >>>>>>>>>> handler getClass : class org.springframework.web.method.HandlerMethod toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person) ========== AnotherInterceptor preHandle ========== >>>>>>>>>> handler getClass : class org.springframework.web.method.HandlerMethod toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person) ========== AnotherInterceptor postHandle ========== isEmpty : true ========== GreetingInterceptor postHandle ========== isEmpty : true ========== AnotherInterceptor afterCompletion ========== ========== GreetingInterceptor afterCompletion ========== |
특별한 order 를 주지 않았을 경우 addInterceptor 순서대로의 order 를 갖는다.
그래서 preHandle 같은 경우 GreetingInterceptor 를 먼저 실행한 것은 너무 당연하다.
하지만 그 다음 postHandle 과 afterCompletion 는 역순으로 실행 되는 것을 볼 수 있다.
Interceptor 를 테스트 해본다고 handler method 에 로그를 남긴 다는 것을 깜빡했다.
다음과 같이 MyHelloController 의 hello 에 로그를 하나 출력 하도록 수정 했다.
package me.nimkoes;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyHelloController {
@GetMapping("/hello")
public String hello(@RequestParam("name") Person person) {
System.out.println("handler method execute !!");
return "hello " + person.getName();
}
}
테스트 코드를 다시 한 번 실행해보면 preHandle 다음에 handler method 의 로그를 출력 하는 것을 볼 수 있다.
========== GreetingInterceptor preHandle ========== >>>>>>>>>> handler getClass : class org.springframework.web.method.HandlerMethod toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person) ========== AnotherInterceptor preHandle ========== >>>>>>>>>> handler getClass : class org.springframework.web.method.HandlerMethod toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person) handler method execute !! ========== AnotherInterceptor postHandle ========== isEmpty : true ========== GreetingInterceptor postHandle ========== isEmpty : true ========== AnotherInterceptor afterCompletion ========== ========== GreetingInterceptor afterCompletion ========== |
둘 중 하나의 preHandle 이 false 를 반환하면 어떤 결과가 나오는지 확인해 보았다.
우선 GreetingInterceptor 의 preHandle 이 false 를 반환하도록 했다.
========== GreetingInterceptor preHandle ========== >>>>>>>>>> handler getClass : class org.springframework.web.method.HandlerMethod toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person) |
당연히 테스트는 실패 했고, GreetingInterceptor 의 preHandle 까지만 실행하고 다음 작업은 하지 않았다.
그럼 반대로 GreetingInterceptor 는 true 를 반환하고 AnotherInterceptor 는 false 를 반환 하도록 해보았다.
========== GreetingInterceptor preHandle ========== >>>>>>>>>> handler getClass : class org.springframework.web.method.HandlerMethod toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person) ========== AnotherInterceptor preHandle ========== >>>>>>>>>> handler getClass : class org.springframework.web.method.HandlerMethod toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person) ========== GreetingInterceptor afterCompletion ========== |
재미있는 결과를 볼 수 있었는데, true 를 반환하는 Interceptor 에 대해서는 afterCompletion 은 실행 하는 것을 볼 수 있다.
결국 Interceptor chain 이 있을 때, 최초로 false 를 반환하는 Interceptor 까지는 afterCompletion 을 실행 하는것 같다.
그리고 하나라도 false 를 반환 한다면 handler method 는 실행되지 않는다는 것도 확인 할 수 있었다.
마지막으로 Interceptor chain 을 구성할 때 등록 순서에 상관 없이 order 를 줄 수 있다.
package me.nimkoes;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new GreetingInterceptor()).order(0);
registry.addInterceptor(new AnotherInterceptor()).order(-1);
}
}
숫자가 낮을수록 높은 우선순위를 갖는다.
따라서 지금 등록한 두 개의 Interceptor 의 preHandle 이 모두 true 를 반환 하도록 수정한 다음 테스트 코드를 실행하면 아까와는 다른 실행 순서를 확인할 수 있다.
========== AnotherInterceptor preHandle ========== >>>>>>>>>> handler getClass : class org.springframework.web.method.HandlerMethod toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person) ========== GreetingInterceptor preHandle ========== >>>>>>>>>> handler getClass : class org.springframework.web.method.HandlerMethod toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person) handler method execute !! ========== GreetingInterceptor postHandle ========== isEmpty : true ========== AnotherInterceptor postHandle ========== isEmpty : true ========== GreetingInterceptor afterCompletion ========== ========== AnotherInterceptor afterCompletion ========== |
이번에는 AnotherInterceptor 가 더 높은 우선순위를 가지고 실행된 것을 볼 수 있다.
그리고 만약 Interceptor 를 등록한 것은 좋은데, 모든 handler 에 대해 적용 되기를 바라지 않을 수 있다.
이런 경우 pattern 을 설정하여 특정 요청에 대해서만 Interceptor 가 동작하도록 할 수 있다.
다음은 GreetingInterceptor 에 대해 /hi 요청에 대해서만 동작하도록 설정한 코드이다.
package me.nimkoes;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new GreetingInterceptor())
.addPathPatterns("/hi")
.order(0);
registry.addInterceptor(new AnotherInterceptor()).order(-1);
}
}
========== AnotherInterceptor preHandle ========== >>>>>>>>>> handler getClass : class org.springframework.web.method.HandlerMethod toString : public java.lang.String me.nimkoes.MyHelloController.hello(me.nimkoes.Person) handler method execute !! ========== AnotherInterceptor postHandle ========== isEmpty : true ========== AnotherInterceptor afterCompletion ========== |
테스트 코드는 /hello 요청을 보내기 때문에 AnotherInterceptor 만 동작한 것을 확인할 수 있다.
등록한 각각의 Interceptor 에 대해 order 와 pattern 을 적정하게 사용하면 중복 코드를 줄이고 관리하기 편한 애플리케이션을 만들 수 있을것 같다.
'Archive > Spring Web MVC' 카테고리의 다른 글
2.6 HTTP 메시지 컨버터 (JSON, XML) (0) | 2021.07.09 |
---|---|
2.5 리소스 핸들러 (default Servlet) (1) | 2021.07.08 |
2.3 Spring Web MVC 의 Formatter (Spring Boot 자동 설정) (0) | 2021.07.06 |
2.2 Spring Boot 의 Spring MVC 설정 (war 배포) (0) | 2021.07.05 |
2.1 Spring Web MVC 설정 @EnableWebMvc, WebMvcConfigurer (0) | 2021.07.04 |