스프링 인터셉터

바그다드·2023년 5월 20일
0
post-thumbnail
  • 서블릿 필터와 마찬가지로 웹과 관련된 공통 관심 사항을 해결할 수 있는 기술
  • 필터가 서블릿에서 제공하는 기술인 것과 달리, 인터셉터는 스프링에서 제공하는 기술이다.

1. 흐름

로그인 사용자 : HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
비로그인 사용자 : HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터(컨트롤러를 호출하지 않고 끝냄)

  • 스프링 MVC가 제공하는 기능이기 때문에 디스패처 서블릿 이후에 등장하게 된다.
    스프링 MVC의 시작점이 디스패처 서블릿이기 때문

2. 스프링 인터셉터 체인

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러

  • 인터셉터도 필터와 마찬가지로 체인으로 구성되어 여러 개의 인터셉터를 추가할 수 있다.
  1. preHandle
    컨트롤러 이전에 호출되어 응답값이 true면 진행하고, false면 진행하지 않는다.
  2. postHandle
    컨트롤러 호출 후에 호출된다. (더 정확히는 핸들러 어댑터 호출 후에 호출된다.)
    컨트롤러에서 예외가 발생하면 postHandle은 호출되지 않는다.
  3. afterCompletion
    뷰가 렌더링 된 이후에 호출된다.
    컨트롤러에서 예외가 발생하더라도 호출되므로 예외와 무관하게 진행되어야 할 것이 있다면 afterCompletion을 사용하자

3. 로그 인터셉터

  • 그럼 이제 코드로 인터셉터를 활용해보고 필터와의 차이를 확인해보자

1. 인터셉터 생성

@Slf4j
public class LogInterceptor implements HandlerInterceptor {

    public static final String LOG_ID = "logId";
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String requestURI = request.getRequestURI();
        String uuid = UUID.randomUUID().toString();

        //하나의 요청 흐름에서 request는 같은 객체이므로 request객체를 통해 값 전달
        request.setAttribute(LOG_ID, uuid);

        // @RequestMapping : HandlerMethod
        // 정적 리소스 : ResourceHttpRequestHandler
        if (handler instanceof HandlerMethod) {
            // 호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
            HandlerMethod hm = (HandlerMethod) handler;
        }

        log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
        // true면 다음 단계 진행, false면 중단
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandler [{}]", modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        String requestURI = request.getRequestURI();
        Object logId = request.getAttribute(LOG_ID);

        log.info("RESPONSE [{}][{}][{}]", logId, requestURI, handler);
        if (ex != null) {
            log.error("afterCompletion error!!", ex);
        }
    }
}
  • 인터셉터는 필터와 달리 메서드가 default로 정의되어 있어 필요한 것만 override해서 사용하면 된다.
  • HandlerMethod
    스프링은 일반적으로 @Controller , @RequestMapping 을 활용한 핸들러 매핑을 사용하는데, 이때 컨트롤러 메서드의 모든 정보가 HandlerMethod에 담겨 넘어온다.
  • ResourceHttpRequestHandler
    @Controller 가 아니라 /resources/static 와 같은 정적 리소스가 호출 되는 경우에는
    ResourceHttpRequestHandler 가 핸들러 정보로 넘어오기 때문에 타입에 따라서 처리해주자
  • request.setAttribute(LOG_ID, uuid);
    필터의 경우에는 필터의 시작부터 끝까지 하나의 메서드에서 이루어지기 때문에 지역변수로 같은 logid값을 사용할 수 있었지만, 인터셉터는 분리되어 있는데, 스프링은 기본적으로 싱글톤으로 이루어져 있기 때문에 멤버변수를 활용하기에는 문제가 있을 수 있다.
    대신에 하나의 요청에 대해서는 동일한 객체를 유지하는 request를 이용하여 값을 공유하자.

2. 인터셉터 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico", "/error");

        registry.addInterceptor(new LoginCheckInterceptor())
                .order(2)
                .addPathPatterns("/**")
                .excludePathPatterns("/", "/members/add", "/login", "/logout",
                        "/css/**", "/*.ico", "/error");
    }
}
  • WebMvcConfigurer가 제공하는 addInterceptors()메서드로 인터셉터를 등록하자.
  • registry.addInterceptor(new LogInterceptor())
    인터셉터를 등록하는 메서드이다.
  • order()
    인터셉터 우선순위를 정하는 메서드이다.
  • addPathPatterns()
    인터셉터를 적용할 URL패턴을 지정할 수 있다.
  • excludePathPatterns()
    인터셉터에서 제외시킬 URL패턴을 지정할 수 있다.

    필터와 달리 addPathPatterns와 excludePathPatterns메서드를 활용하여 정교한 URL패턴 설정이 가능하다.

2. 로그인 인터셉터

1. 인터셉터 생성

@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {

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

        String requestURI = request.getRequestURI();

        log.info("인증 체크 인터셉터 실행 {}", requestURI);

        HttpSession session = request.getSession();

        if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
            log.info("미인증 사용자 요청");
            //로그인으로 redirect
            response.sendRedirect("/login?redirectURL=" + requestURI);
            return false;
        }

        return true;
    }
}
  • HandlerInterceptor의 메서드는 default로 정의되어 있기 때문에 필요한 메서드만 override해서 사용하면 된다.
  • 여기서는 로그인 체크만 하면 되므로 preHanlder를 사용하자
  • 또 필터와 다른점은 필터의 경우 whiteList라는 리스트를 만들어 로그인 체크를 할 페이지를 따로 검사하는 로직을 생성했는데, 인터셉터에서는 그럴 필요없이 인터셉터를 등록할 때 로그인을 체크할 URL패턴을 설정할 수 있다.

필터 등록

  • 기존에 생성되어 있는 addInterceptor()메서드에 아래 코드를 추가해주자
		// 윗부분(로그 인터셉터 등록 코드) 생략
        registry.addInterceptor(new LoginCheckInterceptor())
                .order(2)
                .addPathPatterns("/**")
                .excludePathPatterns("/", "/members/add", "/login", "/logout",
                        "/css/**", "/*.ico", "/error");
  • addPathPatterns와 excludePathPatterns을 활용해 로그인이 필요한 페이지를 세밀하게 조절할 수 있다.

PathPattern 공식 문서

  • 다음은 스프링에서 제공하는 URL 경로이다. 서블릿보다 더 자세하게 설정할 수 있으므로 다음을 참고하자.
? 한 문자 일치
* 경로(/) 안에서 0개 이상의 문자 일치
** 경로 끝까지 0개 이상의 경로(/) 일치
{spring} 경로(/)와 일치하고 spring이라는 변수로 캡처
{spring:[a-z]+} matches the regexp [a-z]+ as a path variable named "spring"
{spring:[a-z]+} regexp [a-z]+ 와 일치하고, "spring" 경로 변수로 캡처
{*spring} 경로가 끝날 때 까지 0개 이상의 경로(/)와 일치하고 spring이라는 변수로 캡처
/pages/t?st.html — matches /pages/test.html, /pages/tXst.html but not /pages/
toast.html
/resources/*.png — matches all .png files in the resources directory
/resources/** — matches all files underneath the /resources/ path, including /
resources/image.png and /resources/css/spring.css
/resources/{*path} — matches all files underneath the /resources/ path and
captures their relative path in a variable named "path"; /resources/image.png
will match with "path" → "/image.png", and /resources/css/spring.css will match
with "path" → "/css/spring.css"
/resources/{filename:\\w+}.dat will match /resources/spring.dat and assign the
value "spring" to the filename variable

링크: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html

  • 필터와 인터셉터 모두 웹과 관련된 공통 관심사를 해결하기 위한 기술이다.
    아무 기술이나 사용해도 상관없지만 특별히 문제가 있는게 아니라면 개발자 입장에서 편리한 인터셉터를 사용하는게 좋을 거 같다.

출처 : 김영한-스프링MVC2편

profile
꾸준히 하자!

0개의 댓글