Java & Spring

Interceptor

밍 끄적 2023. 5. 23. 20:03
728x90

2023.05.16 - [Java & Spring] - Filter

지난번에 정리한 Filter에 이어 Interceptor를 정리하였다.

Interceptor

Interceptor 인터셉터


Interceptor는 Filter, Dispatcher Servlet이 실행된 후 실행되고, Spring Context 영역에서 실행된다.

 

Interceptor는 위의 이미지 처럼, Spring Context 영역에서 실행되는, Spring이 제공하는 기술이다. 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있는 기능을 제공한다.

 

Filter 이후에, Spring Context 영역에서 실행되는 Dispatcher Servlet은 요청에 따른 컨트롤러를 매칭시키위해, 핸들러 매핑을 수행하고 HandlerExecutionChain을 리턴한다.

 

FilterChain이 각 필터를 순서에 따라 다 거친 뒤에 서블릿을 호출한 것 처럼, HandlerExecutionChain은 각 인터셉터를 순서에 따라 다 거친 뒤에 컨트롤러를 호출한다.

Spring docs에 따르면, Hander Excecution chaing은 핸들러 객체와 핸들러 인터셉터로 이루어져있고, HandlerMapping 인터페이스의 getHandler 메소드를 통해 리턴된다.

 

Dispatcher Servlet의 동작 방식

  1. 필터 등을 거친 뒤, Dispatcher Servlet이 클라이언트 요청을 받는다.
    1. Dispatcher Servlet은 doService 메소드를 수행하면서, 필요한 속성값들을 세팅한다.
    2. 이후, doDispatch 메소드를 수행해 아래 작업을 처리한다.
  2. Dispatcher Servlet은 요청을 보고 이 요청을 위임할 컨트롤러를 찾는다. : Handler Mapping
  3. 찾은 컨트롤러로 요청을 위임하기 위해서, 이 컨트롤러에 대한 핸들러 어댑터를 찾아서, 어댑터를 이용해 요청을 전달한다. : Handler Adapter
  4. 핸들러 어댑터가 요청을 컨트롤러로 위임한다.
  5. 비지니스 로직을 수행한다.
  6. 컨트롤러가 Response를 반환한다.
  7. 핸들러 어댑터가 전달받은 Response를 처리하여, Dispatcher Servlet으로 전달한다.
  8. 클라이언트로 전달받은 Response를 반환하여 응답을 마무리한다.

 

핸들러와 컨트롤러

스프링 MVC는 웹 요청을 처리할 수 있는 범용적인 프레임워크를 제공하고있다.

이에 따라, 스프링 MVC는 웹 요청을 실제로 처리하는 객체를 핸들러(Handler)라고 표현하고 있다.

@Controller 적용 객체나 Controller 인터페이스를 구현한 객체 모두 스프링 MVC입장에서는 핸들러이다.

 

Dispatcher Servlet이 핸들러(컨트롤러)를 찾는 방법

Dispatcher Servlet은 요청을 확인해서 적절한 컨트롤러를 찾는데,
이 때, “적절한 컨트롤러를 찾는 것”은 RequestMappingHandlerMapping이 찾아준다.

 

RequestMappingHandlerMappingHandlerMapping의 구현체인데,
HandlerMapping은 HTTP 요청과 핸들러를 매핑 해주는 역할을 하는 인터페이스이다.

 

HandlerMappinggetHandler 메소드를 통해서 주어진 HTTP 요청에 매핑된 핸들러를 반환한다. 정확히는 매핑된 핸들러를 포함한 HandlerExecutionChain을 반환한다.

HandlerMapping 인터페이스는 getHandler 메소드를 가지고 있고, HandlerExecutionChain 를 리턴한다.

RequestMappingHandlerMapping이 상속하고 있는 클래스들을 통해서 getHandler 메소드가 구현된 내부를 살펴보면, Map에 path(경로, T)를 key로, 매핑된 핸들러를 value로 등록하여 관리한다.

 

RequestMappingHandlerMapping 이 상속하는 RequestMappingInfoHandlerMapping 클래스가 상속하는 AbstractHandlerMethodMapping 클래스 내부에서 매핑된 핸들러를 관리하는 MappingRegistry 이다.

MappingRegistry가 관리하는 MappingRegistration을 보면, 매핑될 경로 mappingHandlerMethod를 관리하고 있다.

 

AbstractHandlerMethodMapping 클래스 내부에서는 매핑된 각 핸들러를 MappingRegistration 이라는 클래스로 관리한다.

 

HandlerMethod는 매핑할 경로에 해당하는 컨트롤러 메소드를 가진다.

컨트롤러 메소드를 그냥 사용하지 않고, HandlerMethod로 감싸서 관리하는 이유는 HandlerMethod가 메소드와 함께 메소드에 대한 부가적인 정보를 가지고 있기 때문이다.

Spring docs에 따르면, HandlerMethod는 메소드와 빈으로 구성되어 있고, 메소드에 대한 파라미터, 리턴 값, 메소드 어노테이션 등의 정보에 편리하게 접근할 수 있게 제공한다.

 

이렇게 코드를 열어서 확인하는 이유는,
HandlerMapping을 거치면, 컨트롤러를 바로 반환하는 것이 아니라,
주어진 요청 경로에 매핑된 컨트롤러 메소드를 가지고 있는 HandlerMethod를 포함한,
HandlerExecutionChain를 반환한다는 것이다.

 

정리하면,

  1. Dispatcher Servlet이 HandlerMapping 인터페이스의 getHandler 메소드를 호출한다.
  2. HandlerMapping의 구현체 RequestMappingHandlerMapping가 주어진 경로에 매핑된 핸들러(인터셉터, 컨트롤러)를 가진 HandlerExecutionChain을 반환한다.

 

요청을 핸들러로 바로 위임하지 않고, 핸들러 어댑터를 통해 위임, 전달하는 이유

어차피 요청은 컨트롤러에서 비즈니스 로직을 수행하며 처리할 것이고, Dispatcher Servlet이 컨트롤러를 찾았는데 왜 바로 컨트롤러로 넘기지 않고, 핸들러 어댑터를 통해 요청을 위임하는 걸까?

위에서 설명한 과정을 통해 Handler Mapping으로 HandlerExecutionChain을 받았으면, Chain에서 Interceptor를 꺼내 잘 실행하고, Handler Method를 잘 꺼내서 컨트롤러 메소드에 위임하면 되지 않을까?

맞다. 전달받은 HandlerExecutionChain을 통해 컨트롤러(핸들러)를 실행하면 된다. 하지만, 핸들러의 구현 방식이 다양할 수 있기 때문이다.

 

DispatcherServlet은 핸들러 객체의 실제 타입에 상관없이, 실행 결과를 ModelAndView라는 타입으로만 받을 수 있으면 된다. 그런데 핸들러의 실제 구현 타입에 따라 ModelAndView를 리턴하는 객체도있고, 그렇지 않은 구현 객체도 있다. HandlerAdapter가 바로 이변환을 수행해준다.

 

즉, 스프링의 역사를 지나오면서 핸들러를 다양한 방식으로 구현하였고, 이에 대응하기 위해 HandlerAdapter 인터페이스를 통해 어댑터 패턴을 적용하여 핸들러의 구현방식에 제약없이 요청을 위임할 수 있다. 서블릿과 핸들러 간에 느슨한 결합이 이루어진 것이다.

 

Spring docs에 따르면, 모든 핸들러는 이 HandlerAdapter를 구현하도록 되어 있다. HandlerAdapter를 통해서, DispatcherServlet은 HandlerAdapter 인터페이스를 통해 설치된 모든 핸들러에 접근할 수 있다.

 

응답 전달시, 핸들러 어댑터가 하는 역할

그렇다면, 비즈니스 로직을 처리한 컨트롤러가 전달하는 응답은 왜 핸들러 어댑터를 거쳐야할까?

컨트롤러가 ResponseEntity를 반환하면, 핸들러 어댑터를 거치면서 전달받은 응답을 적절한 형태로 변환한다.

HttpEntityMethodProcessor가 MessageConverter를 사용해 응답 객체를 직렬화하고 응답 상태(HttpStatus)를 설정한다.

 

인터셉터의 실행시점


아래 코드는 Dispatcher Servlet의 doDispatch 메소드, processDispatchResult 메소드의 일부이다.

doDispatch 메소드 코드 일부

doDispatch 메소드 코드 일부를 보면,
getHandlerHandlerExecutionChain을 찾고,
getHandlerAdapterHandlerAdapter을 찾고,
HandlerExecutionChain.applyPreHandle을 통해 핸들러 실행 전에 인터셉터를 실행하고,
HandlerAdapter.handle로 핸들러 어댑터를 통해 핸들러를 invoke하여 비즈니스 로직을 수행하고,
HandlerExecutionChain.applyPostHandle을 통해 핸들러 실행 후에 인터셉터를 실행하고,
processDispatchResult 메소드를 수행한다.

processDispatchResult 메소드 코드 일부

processDispatchResult 메소드 코드 일부를 보면,
HandlerExecutionChain.triggerAfterCompletion로 요청 처리가 다 끝나고 마지막으로 인터셉터를 수행한다.

 

이렇게, 인터셉터는 총 3번의 시점에서 실행된다.

따라서, 인터셉터 인터페이스는 각 시점에서 실행하는 메소드 3개를 가지고 있다.

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 {
    }
}

 

서블릿 필터는 init, doFilter, destory 메소드를 가지고 있었다.

이중, doFilter 메소드 하나로 FilterChain을 실행했는데, 인터셉터는 컨트롤러 호출 전( preHandle ), 호출 후( postHandle ), 요청 완료 이후( afterCompletion )와 같이 단계적으로 잘 세분화 되어 있다.

 

preHandle 메소드 - 핸들러(컨트롤러)가 invoke되기 전에 인터셉터가 실행된다.

핸들러 어댑터가 핸들러를 invoke하기 전에, 인터셉터를 호출한다.

 

preHandle 메소드가 true를 리턴하면 DispatcherServlet와 HandlerExecutionChain은 그 다음 로직을 수행한다. 하지만, false를 리턴한다면, 핸들러 어댑터가 실행되지도, HandlerInterceptor의 postHandle이 실행되지도 않는다. ( 예외 처리를 위해 afterCompletion은 실행된다. )

 

preHandle 시에 처리되는 인터셉터는 컨트롤러 이전에 처리해야 하는 전처리 작업을 수행하거나, 요청 정보를 가공하거나 추가하는 경우에 적합하다.

postHandle 메소드 - 핸들러가 정상적으로 수행된 후에 인터셉터가 실행된다.

핸들러 어댑터가 핸들러를 invoke하여 비즈니스 로직을 수행한 후에, 인터셉터를 호출한다.

 

preHandle 메소드에서 혹은 핸들러가 처리되면서 예외가 발생하면 postHandle의 인터셉터는 실행되지 않는다.

핸들러 처리 이후, 필요한 후 작업이 필요한 경우에 적합하다.

afterCompletion 메소드 - 모든 처리를 마친 후 마지막 인터셉터가 실행된다.

모든 작업, 처리를 마친 후에(뷰가 렌더링된 후에), 인터셉터를 호출한다.

 

예외가 발생해도, afterCompletion은 반드시 호출된다.

 

요청 처리 중에 사용한 리소스를 반환할 때 적합하다.

또, 세번째 파라미터 ex가 예외를 받으므로, 어떤 예외가 발생했는지 로그로 출력할 때에도 적합하다. postHandle도 핸들러 처리 후에 호출되므로 예외 처리를 하기 좋다고 생각할 수 있지만, postHandle은 핸들러 처리 중 예외가 발생하면 호출되지 않으므로, 예외와 상관없이 반드시 호출되는 afterCompletion에서 예외와 무관한 공통적인 예외 처리를 하는 것이 좋다.

 

Interceptor의 활용 예시


 

  • 세부적인 보안 및 인증 / 인가 공통 작업
    • 스프링의 지원을 받으므로 보다 세부적인 작업이 가능하다
    • 공통적인 보안 작업은 Filter에서 처리하는 것이 좋다.
  • API 호출에 대한 로깅, 감사
  • 핸들러(컨트롤러)로 넘겨주는 데이터 가공

 

Interceptor vs AOP


AOP를 복기 시켜보면, 동일하게 어떠한 부가 로직을 수행하기 위함이므로, 인터셉터나 필터나 AOP나 공통적인 목적은 동일하다.

순서

컴파일시에 위빙하는 등 AOP를 구현하는 방법은 3가지가 있지만, 그 중 프록시 위빙을 생각해보면, 특정 메소드에 대한 프록시를 만들어두고, 그 메소드가 호출될때 실제 메소드가 수행되는 구조였다.

그렇기 때문에 순서는 다음과 같을 것이다.

  1. Filter
  2. Interceptor(PreHandle)
  3. AOP
  4. Interceptor(PostHandle, AfterCompletion)
  5. Filter

 

컨트롤러의 부가 로직을 수행하기에 AOP가 좋을까, Interceptor가 좋을까?

컨트롤러는 각 컨트롤러가 내부 파라미터로 구분되기보다는, 매핑되는 경로로 구분된다.

Interceptor는 이에 걸맞게 주소로 핸들러를 매핑하기 때문에 특정 컨트롤러 이전에 부가로직을 수행하기 적합하다.

 

하지만, AOP는 “메소드”를 대상으로한다. 다양한 포인트 컷들을 사용해서 메소드의 실행 전 후에 따라 부가로직을 적용한다.

더보기

 

또, AOP는 JoinPoint로부터 파라미터를 불러와야한다.

AOP도 메소드의 파라미터를 불러올 수는 있다. 아래 코드는 AOP 1편에서 소개했던, 예시 코드이다.

package org.springframework.samples.petclinic.owner;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

@Component
@Aspect
public class LogAspect {

    Logger logger = LoggerFactory.getLogger(LogAspect.class);

    @Around("@annotation(LogExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
                Object[] parameters = joinPoint.getArgs();
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // @LogExecutionTime이 붙어있는 타겟 메소드를 실행
        Object proceed = joinPoint.proceed();

        stopWatch.stop();
        logger.info(stopWatch.prettyPrint());

        return proceed;
    }
}

위 코드에서는 아래와 같이 파라미터를 불러올 수 있다.

Object[] parameters = joinPoint.getArgs();

만약 이 AOP Aspect가 특정 컨트롤러에 수행되고,
request, response 파라미터가 필요하다면(컨트롤러의 부가 로직이니 아마도 필요할 일이 많을 것이다.),
불필요한 로직이 추가되는 것이다.

인터셉터는 아래와 같이 인터셉터의 파라미터로 request, response가 주어진다.

@Component // bean 등록
@RequiredArgsConstructor
public class HasAnimalsInterceptor implements HandlerInterceptor {
    private final AnimalService animalService; // DI

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String email = authentication.getName(); 

        List<Animal> animalList = animalService.findByMemberEmail(email);

        if(animalList.size() == 0){
            response.sendRedirect("/error/not-animals");
            return false;
        }

        return true;
    }
}

따라서 컨트롤러의 부가 로직을 수행하기에는 Interceptor가 더 적합하다.

Filter vs Interceptor


소속된 컨테이너 영역

Filter는 서블릿 컨테이너(톰캣과 같은 웹 컨테이너)의 영역에서 실행된다.

그렇기 때문에 Filter에서 발생하는 예외는 Spring의 예외처리가 불가능하다. 예외가 발생했을 경우, HttpServletResponse에 응답 코드를 설정하고, 에러 응답을 설정하는 등 직접 처리가 필요하다.

 

반면에, Interceptor는 스프링 컨테이너의 영역에서 실행된다.

따라서 스프링의 지원이 필요할 경우에는 Interceptor를 사용하는 것이 좋다.

 

단, Filter도 스프링 빈으로 등록할 수 있고, 빈을 주입받을 수 있다.

 

Request, Response 객체 교체 불가

Filter는 FilterChain에 속하여있는 그 다음의 Filter를 호출할 때, 아래와 같이 호출한다.

public MyFilter implements Filter {

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        // 개발자가 다른 request와 response를 넣어줄 수 있음
        chain.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse());       
    }

}

그렇기 때문에 ServletRequest, ServletReponse 파라미터 객체를 다른 객체로 교체할 수 있다.

 

Interceptor는 HandlerExecutionChain에 속하여있는 그 다음의 인터셉터를 직접 호출하지 않는다.

아래는 HandlerExecutionChain이 각 인터셉터의 preHandle메소드로 호출하여 실행하는 applyPreHandle메소드 코드이다.

호출할 Interceptor들을 for문을 통해, HandlerExecutionChain이 직접 호출한다.

각 Interceptor는 리턴값으로 그 다음 인터셉터를 호출하지 않고, boolean 여부를 리턴한다.

public class MyInterceptor implements HandlerInterceptor {

    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // Request/Response를 교체할 수 없고 boolean 값만 반환할 수 있다.
        return true;
    }

}

 

전달받는 파라미터

Filter 구현체 코드를 보면, Request, Response 그리고 그 다음 Filter를 호출하기 위한 FilterChain이 파라미터로 주어진다.

public MyFilter implements Filter {

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        // 개발자가 다른 request와 response를 넣어줄 수 있음
        chain.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse());       
    }

}

 

반면에, Interceptor 구현체 코드를 보면, Request, Response 그리고 핸들러가 파라미터로 주어진다.

주어진 핸들러를 통해 어떤 modelAndView가 반환되었는지의 응답정보 등을 알 수 있다.

public class MyInterceptor implements HandlerInterceptor {

    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // Request/Response를 교체할 수 없고 boolean 값만 반환할 수 있다.
        return true;
    }

}

따라서, 핸들러의 정보가 필요한 경우 Interceptor가 적합할 것이다.

전체 구조


Reference


https://velog.io/@timssuh/Spring-Boot에서-커스텀-Interceptor-사용해보기

 

Spring Boot에서 커스텀 Interceptor 사용해보기

상황 온라인 동물병원 프로젝트 예약서는 동물을 갖고 있는 고객만이 들어갈 수 있도록 한다. 매번 검사할 수 있지만, 너무 중복이 많다. 공통처리. 그중 Interceptor를 사용해보자!. 사전 지식 Interc

velog.io

https://mangkyu.tistory.com/173

 

[Spring] 필터(Filter) vs 인터셉터(Interceptor) 차이 및 용도 - (1)

Spring은 공통적으로 여러 작업을 처리함으로써 중복된 코드를 제거할 수 있도록 많은 기능들을 지원하고 있다. 이번에는 그 중에서 필터(Filter) vs 인터셉터(Interceptor)의 차이에 대해 알아보고자

mangkyu.tistory.com

https://tecoble.techcourse.co.kr/post/2021-07-15-dispatcherservlet-part-2/

 

DispatcherServlet - Part 2

지난 1편에서는 DispatcherServlet 정의, 설정 방법, 동작 흐름에 대해 알아봤다. 이번 2편에서는 DispatcherServlet의 동작 원리를 코드와 함께 살펴보자.

tecoble.techcourse.co.kr

https://docs.spring.io/spring-framework/docs/4.2.8.RELEASE_to_4.2.9.RELEASE/Spring%20Framework%204.2.9.RELEASE/org/springframework/web/servlet/HandlerExecutionChain.html#applyPreHandle-HttpServletRequest-HttpServletResponse-

 

HandlerExecutionChain

(package private) void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, java.lang.Exception ex) Trigger afterCompletion callbacks on the mapped HandlerInterceptors.

docs.spring.io

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/handler/MappedInterceptor.html#preHandle(jakarta.servlet.http.HttpServletRequest,jakarta.servlet.http.HttpServletResponse,java.lang.Object)

 

MappedInterceptor (Spring Framework 6.0.9 API)

postHandle Interception point after successful 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.

docs.spring.io

https://github.com/binghe819/TIL/blob/master/Spring/MVC/Spring%20MVC%20flow.md

 

GitHub - binghe819/TIL: 📚 Today I Learned. 기록하자.

📚 Today I Learned. 기록하자. Contribute to binghe819/TIL development by creating an account on GitHub.

github.com

 

728x90