Archive/Spring Web MVC

2.4 핸들러 인터셉터 (HandlerInterceptor interface)

nimkoes 2021. 7. 7. 01:32
728x90

 

 

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 을 적정하게 사용하면 중복 코드를 줄이고 관리하기 편한 애플리케이션을 만들 수 있을것 같다.

 

 

 

 

728x90