프로그래밍/JAVA Spring

[Spring 스프링] HandlerInterceptor 알아보기

hectick 2023. 5. 7. 23:07

 

인터셉터는 전체 path, 또는 특정 path에 대하여 컨트롤러가 실행되기 전 후로, 요청과 응답을 가로채서 적절한 전/후 처리를 하도록 도와준다. 핸들러에 사용자 인증과 같은 중복 코드가 존재할 때, 이를 인터셉터로 옮기면 핸들러의 중복코드를 줄일 수 있다.

 

다음은 인터셉터를 구현할 때 사용하는 HandlerInterceptor 인터페이스이다.

    public interface HandlerInterceptor {

        default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
                throws Exception {

            return true;
        }

        default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                @Nullable ModelAndView modelAndView) throws Exception {
        }

        default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
                @Nullable Exception ex) throws Exception {
        }

    }

 

위의 메서드들을 차례대로 알아보자. 

 

 

preHandle

 

이 메서드는 HandlerMapping이 적절한 handler 객체를 결정 한 후, 그러나 HandlerAdapter가 handler를 호출하기 전에 호출 된다. 간단히 말하면, 컨트롤러가 실행되기 전에 호출된다.

 

1. HandlerMapping이 handler 결정

2. Interceptor의 preHandle 메서드 실행

3. HandlerAdapter가 handler 실행

 

반환값이 true이면 다음 인터셉터, 또는 handler가 실행되고, false면 여기서 중단된다.

 

 

postHandle

 

이 메서드는 HandlerAdapter가 handler를 호출한 후에 호출되지만, DispatcherServlet이 view를 렌더링 하기 전에 호출된다. 간단히 말하면, 컨트롤러가 성공적으로 실행된 후인데, view가 렌더링 되기 전에 호출된다.

 

1. HandlerAdapter가 handler 실행

2. Interceptor의 postHandle 메서드 실행

3. DispatcherServlet이 view를 렌더링

 

RestController를 쓰면 반환할 view가 없는데? 그러면 postHandle이 동작할까? 라는 생각이 들어서 postHandle과 afterCompletion 메서드가 호출되었을 때 로그가 찍히도록 해보았다. view를 렌더링할 필요가 없어도, postHandle -> afterCompletion 순서로 잘 작동한다.

 

preHandle의 ModelAndView 파라미터를 보면 @Nullable 어노테이션이 달려있는데, 아마 지금처럼 view를 렌더링할 필요가 없을 때도 작동될 수 있게 하기 위해 달아 둔 것 같다.

 

 

afterCompletion

 

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.

Note: Will only be called if this interceptor's preHandle method has successfully completed and returned true!

 

요청 처리가 성공적으로 끝난 후, 즉 view가 렌더링 된 후에 호출된다.

 

handler 실행 결과에 관계없이 항상 호출되기 때문에, 적절한 자원 정리를 할 수 있다.

preHandle 메서드가 성공적으로 실행되어 true를 반환한 경우에만 호출된다.

 

바로 위의 두 문장은 공식문서에 나와있는 내용인데 솔직히 이해가 안된다. "handler 실행 결과"라는 것이 handler의 실행 여부인지, 아니면 handler가 실행되고 나서 예외가 터졌는지 여부인지 헷갈린다. 그래서 이 포스팅의 끝자락에서 직접 코드를 까보며 이해해 보았다.

 

또, postHandle이 ModelAndView가 @Nullable인 것과 비슷하게, afterCompletion도 Exception이 @Nullable이다. 이 것은 afterCompletion은 예외가 터져도 안터져도 동작한다는 의미가 아닐까? handler가 실행되었는데 예외가 발생했을 경우, exceptionHandler를 통해 예외를 처리하고, 핸들러의 실행 결과가 정해지는 것 같다는 것이 나의 뇌피셜이다.

 

 

Interceptor에서 preHandle 빠꾸 당하는 경우 afterCompletion이 호출되는지 실험

 

결론부터 말하면, preHandle에서 예외가 터졌을 경우엔 preHandle만 실행되고 postHandle, afterCompletion은 실행되지 않는다.

(단, 인터셉터가 1개인 경우만 해당한다. 인터셉터가 2개 이상인 경우에는 afterCompletion이 실행되기도 하는데, 아래에서 실제 코드를 까볼 때 자세히 알아볼것이다.)

 

인터셉터를 통해 사용자 인증을 반드시 거쳐야하는 URI인데, 사용자 정보 없이 그냥 날려보았다.

 

로그를 찍어보니 preHandle 메서드만 동작하였다.

 

 

 

뇌피셜 검증: 컨트롤러에서 예외가 터진경우 실험

 

그렇다면 handler에서 예외가 터진 경우는 어떨까? 뇌피셜을 마저 검증해보자.

결론부터 말하면 handler에서 예외가 터진 경우엔 postHandle은 작동하지 않고, afterCompletion은 작동한다.

(handler가 실행되었기 때문에 preHandle은 당연히 작동)

 

Interception point after successful execution of a handler. - postHandle 설명 

 

postHandle의 경우에는 공식문서에 위의 문구가 있다. successful이란 것이 있는 것을 보면, postHandle은 예외가 발생했을 경우에는 작동하지 않는 것 같아서, 한번 존재하지 않는 상품을 장바구니에 담는 요청을 보내고 로그를 찍어보았다. postHandle은 작동하지 않고, afterCompletion만 작동한다.

 

 

 

인터셉터를 여러개 등록했다면, 인터셉터는 어떤 순서로 호출될까? with HandlerExecutionChain 클래스

 

DispatcherServlet은 handler를 실행 체인(execution chain)에서 처리한다. handler가 실행 체인의 제일 마지막에 위치하며, 여러개의 인터셉터로 구성될 수도 있다. 다음 사진은 HandlerExecutionChain 클래스의 필드를 가져온 것이다. 인터셉터의 리스트와 handler를 필드로 가지고 있는 것을 알 수 있다.

 

 

여러개의 인터셉터가 실행 체인에 등록되었을 경우. 호출되는 메서드가 무엇인지에 따라 인터셉터의 호출 순서가 바뀐다. preHandle 메서드에 대해서는 인터셉터가 순차적으로, postHandle과 afterCompletion에 대해서는 인터셉터가 역순으로 호출된다.

 

한번 직접 실험해 보았다. 다음 코드는 이번 장바구니 미션 코드인데, 로그만 찍는 SecondInterceptor를 두번째로 추가해보았다.

    @Configuration
    public class WebMvcConfiguration implements WebMvcConfigurer {

        private final AuthenticationInterceptor authenticationInterceptor;

        public WebMvcConfiguration(final AuthenticationInterceptor authenticationInterceptor) {
            this.authenticationInterceptor = authenticationInterceptor;
        }

        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(authenticationInterceptor)
                    .addPathPatterns("/cart/cart-items/**");
            registry.addInterceptor(new SecondInterceptor())
                    .addPathPatterns("/cart/cart-items/**");
        }

    }

 

위에 실행 결과는 다음과 같다.

 

1. 첫번째 인터셉터의 preHandle 호출

2. 두번째 인터셉터의 preHandle 호출

3. 두번째 인터셉터의 postHandle 호출

4. 첫번째 인터셉터의 postHandle 호출

5. 두번째 인터셉터의 afterCompletion 호출

6. 첫번째 인터셉터의 afterCompletion 호출

 

 

이제 HandlerExecutionChain 클래스의 메서드를 살펴보자.

 

HandlerExecutionChain 클래스의 applyPreHandle 메서드

 

다음 사진은 applyPreHandle 이라는 메서드인데,  0부터 interceptorList의 사이즈-1 까지 인덱스를 순회하면서 interceptorList에 들어있는 intercepter들의 preHandle 메서드를 호출한다. 그리고 만약 intercepter의 preHandle 메서드가 false를 반환했을 경우에는 afterCompletion을 실행하고 반복문을 탈출한다.

 

 

다시보니까 위의 나의 실험에서는 인터셉터가 1개 뿐이었다. 그 1개에서 예외가 터져버렸으므로 작동될 afterCompletion가 없었다. 그래서 인터셉터를 두개 만들어서 다시 실험해보았다. 두번째 인터셉터의 preHandle에서 예외가 터졌는데, 첫번째 인터셉터의 afterCompletion만 동작한 것을 확인하였다. (그런데 나는 예외가 터지게 할 뿐 실패했을때 따로 false를 반환해주는 코드를 짜지는 않았는데, 예외가 터지면 자동으로 false가 반환되는 것인가..? 그것은 아직 미해결이다.)

 

 

아무튼 어서 결론을 짓자면, preHandle과정에서 특정 인터셉터의 차례에서 예외가 터졌을 경우, 그 인터셉터 바로 앞까지의 인터셉터만 afterCompletion 과정을 거친다고 정리할 수 있다. 어느 인터셉터부터 afterCompletion을 해야하는지 기록하는 것이 필요하기 때문에 preHandle이 성공적으로 끝나면 interceptorIndex = i 로 갱신해주고, 이것을 추후에 afterCompletion 과정에서 사용하는 것이다.

 

아무튼 이제 나는 아래의 문장을 비로소 이해할 수 있게 되었다.

 

afterCompletion은 handler 실행 결과(여부)에 관계없이 항상 호출되기 때문에, 적절한 자원 정리를 할 수 있다.

단, preHandle 메서드가 성공적으로 실행되어 true를 반환한 경우에만 호출된다.

 

 

HandlerExecutionChain 클래스의 triggerAfterCompletion 메서드

 

이전에 interceptorIndex를 이용해서 반복문을 순회한다. 이번에는 역순으로 interceptorIndex 부터 0까지의 인덱스에 해당하는 interceptorList에 있는 interceptor들의 afterCompletion 메서드를 호출한다. 만약 preHandle 과정에서 모든 인터셉터가 true를 반환하였다면, interceptorIndex는 interceptorList의 사이즈보다 1이 작을 것이다. 즉, 이때는 모든 인터셉터의 afterCompletion 메서드도 호출된다.

 

 

 

HandlerExecutionChain 클래스의 applyPostHandle 메서드

 

별 다른 것은 없다. InterceptorList에 들어있는 인터셉터들이 역순으로 모두 호출된다.