-> 블로그 이전

[Spring] 스프링 인터셉터

2022. 7. 10. 17:17Language`/Spring

 

[Spring] Servlet "Filter"

현재 Controller에 의해서 매핑되는 Page가 다음 종류가 있다고 하자 1. 로그인 하지 않고 접근 가능 2. 로그인 해야 접근 가능 >> 여기서 과연 "로그인 해야 접근 가능한 페이지"가 진짜 로그인을 해야

cs-ssupport.tistory.com

이전에는 서블릿에서 제공해주는 "서블릿 필터"를 통해서 Request에 대한 필터링을 해주었다

이번에는 Spring에서 제공해주는 "스프링 인터셉터"를 알아보자

 

서블릿 필터 & 스프링 인터셉터 모두 웹과 관련된 "공통 관심 사항"을 처리하지만 적용되는 순서도 다르고 적용되는 방법또한 약간 다르다

 


Spring Interceptor

Servlet Filter는 서블릿이 호출되기 전에 수문장 역할을 수행했지만, "Spring Interceptor"는 DispatcherServlet과 Controller사이에서 컨트롤러가 호출되기 직전에 호출된다

 

Spring Interceptor도 마찬가지로 "Chaining"해서 여러 인터셉터들을 엮을 수 있다

그리고 동일하게 인터셉터가 "이 Request는 적절하지 않은 요청같습니다"라고 판단하면 거기서 중단하고 결국 컨트롤러는 호출되지 않는다

 


HandlerInterceptor <Interface>

public class SpringInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
                             Object handler) throws Exception {

    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, 
                           Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                                Object handler, Exception ex) throws Exception {

    }
}

HandlerInterceptor는 총 3개의 메소드로 구현되어 있고 3개의 메소드 전부 default이므로 굳이 구현하지 않고 로직상 필요한 메소드만 구현하면 된다

Inflearn 김영한 : 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술

1. default boolean preHandle

preHandle은 핸들러 어댑터가 호출되기 전에 호출되는 메소드

preHandle의 return type을 살펴보면 "boolean"이다.

▶ true : 다음으로 진행된다

  • 추가적 인터셉터 호출? 핸들러 어댑터 호출?

▶ false : 더는 진행되지 않는다

  • 나머지 인터셉터는 당연히 호출되지 않고, 결론적으로 핸들러 어댑터도 호출되지 않는다 >> 그냥 끝이 나버린다

 

그리고 preHandle의 파라미터 목록을 보면 "Object handler"라는 파라미터가 존재한다

Spring은 굉장히 유연하게 handler를 허용하기 때문이다

  • @Controller, @RequestMapping, ... 과 같은 MVC 관련 handler = "HandlerMethod"
  • 정적 리소스(resource/static/~~~~)들의 handler = "ResourceHttpRequestMethod"

 

2. default void postHandle

postHandle는 컨트롤러가 호출되고 난 후 컨트롤러가 동작을 마치고 핸들러 어댑터를 호출한 후에 호출되는 메소드이다

postHandler은 만약에 "컨트롤러에서 예외가 터졌을 경우"에는 호출되지 않는다

 

Inflearn 김영한 : 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술

 

3. default void afterCompletion

afterCompletion은 View가 Rendering된 이후에 호출되는 메소드이다

postHandle과는 달리 afterCompletion은 "컨트롤러에서 예외가 터지든 말든" 무조건 호출된다

>> 파라미터를 보면 Exception ex가 존재하고 이를 통해서 예외를 afterCompletion에서 확인할 수 있다


(1) 정상 호출 (컨트롤러 : @ResponseBody)

@Slf4j
public class SpringInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("----- preHandle(컨트롤러 호출 전) -----");

        String uuid = UUID.randomUUID().toString();
        log.info("REQUEST -> [{}][{}][{}]", uuid, request.getRequestURL(), request.getMethod());

        return true; // 다음으로 진행
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("----- postHandle(컨트롤러 호출 후) -----");

        String uuid = UUID.randomUUID().toString();
        log.info("REQUEST -> [{}][{}][{}][{}]", uuid, request.getRequestURL(), request.getMethod(), modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("----- afterCompletion(뷰 렌더링 후) -----");

        String uuid = UUID.randomUUID().toString();

        if(ex != null){
            log.info("error Occurred!!!", ex);
        }
        log.info("REQUEST -> [{}][{}][{}]", uuid, request.getRequestURL(), request.getMethod());
    }
}
@ResponseBody
@RequestMapping("/interceptor")
public String interceptor(){
    log.info("컨트롤러 호출!!!");
    return "ok";
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SpringInterceptor())
                .addPathPatterns("/interceptor")
                .order(1);
    }
}

interceptor를 구현하고 사용하려면 스프링 부트상에 등록을 해줘야 한다

이 때 "WebMvcConfigurer"를 implements하고 내부에 "addInterceptors"를 Override해서 등록해주면 된다

  • Spring Interceptor는 Servlet Filter와는 달리 더 정교하고 세밀하게 URL 경로를 설정할 수 있다

 

결과를 보면 컨트롤러 호출 전에 "preHandle"이 호출되고, 컨트롤러 호출 후에 "postHandle"이 호출된다

 

(2) 컨트롤러에서 예외 발생

>> 만약 컨트롤러에서 "예외"가 터지면 어떤일이 발생할까??

@ResponseBody
@RequestMapping("/interceptor")
public String interceptor(){
    log.info("컨트롤러 호출!!!");
    throw new RuntimeException("런타임 예외!!!");
}

컨트롤러에서 예외가 터지면 "postHandle"은 호출되지 않는다. 그에 반해서 afterCompletion은 예외가 터지든 말든 항상 호출된다

그런데 여기서 이상한 점예외가 터졌는데 GET으로 동일한 URI Request가 들어온 것이다

일반적으로 Controller에서 RunTimeException이 터지고 중간에서 catch를 통해서 잡지 않으면 해당 예외는 "WAS"까지 전달이 된다
예외를 받은 WAS는 "이 예외는 서버에서 처리가 안된 예외구나"라고 인식을해서 에러 페이지를 보여주게 된다
>> 이 과정에서 Controller에게 "예외 페이지를 위한 GET 요청"을 보내게 된다

그러면 Client의 GET 요청과 WAS의 예외 페이지를 위한 GET 요청을 구분할 수 있는 방법은 없을까?
>> request의 "DispatcherType"을 보면 Client의 GET 요청은 "REQUEST"이고, 예외 페이지를 위한 GET 요청은 "ERROR"로 표현됨을 확인할 수 있다

<이러한 예외 관련 포스팅은 추후에 예정>

 

(3) 컨트롤러에서 ModelAndView 반환

@RequestMapping("/interceptor")
public String interceptor(Model model){
    log.info("컨트롤러 호출!!!");
    model.addAttribute("data1", "Hello Spring!!");
    model.addAttribute("data2", "JPA!!");
    model.addAttribute("data3", "김영한!!");
    return "home";
}

반환된 View("home")와 컨트롤러에서 View를 위해 담아둔 "Model Value"들의 ModelAndView에 담긴다는 사실을 알 수 있다

 


Spring URL 경로

Spring Interceptor에서는 "addPathPatterns"를 통해서 특정 URL에 인터셉터를 적용할 수 있고, "excludePathPatterns"를 통해서 특정 URL에 인터셉터를 적용하지 않을 수 있다

 

그리고 Spring에서 URL 경로를 설정할 때는 약간 다른 방식으로 설정해준다

 

PathPattern (Spring Framework 5.3.21 API)

Representation of a parsed path pattern. Includes a chain of path elements for fast matching and accumulates computed state for quick comparison of patterns. PathPattern matches URL paths using the following rules: ? matches one character * matches zero or

docs.spring.io

?

"?"은 한 문자 일치를 의미한다

/naver/t?st?.html
→ /naver/testa.html (O)
→ /naver/testP.html (O)
→ /naver/teestO.html (X)

 

*

"*"의 경우 경로 안에서 0개 이상의 문자 일치를 의마한다

/resource/*.html
→ /resource 하위의 모든 html 문서에 대해 매칭된다

 

**

"**"의 경우 경로 끝까지 0개 이상의 경로 일치를 의미한다

/resource/**
→ /resource 하위의 모든 asset에 대해 매칭된다

 

{~~~}

경로와 일치하고 경로상에 {} 부분을 <~~~>로 매핑한다는 의미이다

/resource/{SpringPath}.css
→ /resource/bootstrap.css :: SpringPath = bootstrap

 

{*~~~}

경로가 끝날때까지 0개 이상의 경로와 일치하고 경로상에 {} 부분을 <~~~>로 매핑한다는 의미이다

/resource/{SpringPath}
→ /resource/hello/world/java/python :: SpringPath = hello/world/java/python