Spring - 인터셉터(Interceptor)

컴업·2021년 11월 17일
1

인터셉터 - 소개

스프링에서 제공하는 인터셉터는 서블릿에서 제공하는 필터와 거의 비슷한 기능을 합니다.

서블릿 필터

모든 컨트롤러에서 공통으로 처리해야 할 로직을 각 컨트롤러마다 작성하는 것은 매우 비효율 적일 뿐만 아니라, 이와 관련된 로직이 변경될 경우 모든 컨트롤러에 손을 대야하는 불편함이 있습니다.

이러한 공통 관심사를 한거번에 처리하기 위해, 컨트롤러 로직이 실행되기 전 실행되는 것이 바로 인터셉터입니다.

인터셉터는 다음과 같은 흐름을 가지고있습니다.

인터셉터 - 인터페이스

인터셉터는 아래와 같이 HandlerInterceptor 인터페이스를 구현하여 만듭니다.

public class TempInterceptro implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

doFilter() 하나만 제공하는 필터에 비해서 preHandler(호출전), postHandler(호출후)와 같이 단계적으로 잘 세분화 되어있습니다.

또한 request, response외에도 handler(어떤 컨트롤러가 호출될 지)정보 그리고 ModelAndView정보도 받아볼 수 있습니다.

인터셉터 호출 흐름

  • preHandle : 컨트롤러(좀 더 정확히 핸들러 어댑터)호출 전에 호출된다.
    preHandle의 응답값이 true이면 다음으로 진행.

  • postHandle : 컨트롤러(좀 더 정확히는 핸들러 어댑터) 호출 후에 호출된다.

  • afterCompletion : 뷰가 렌더링 된 이후에 호출된다.

인터셉터 예외(Exception) 발생

컨트롤러에서 예외가 발생하면 postHandle은 호출되지 않고 WAS로 예외가 전달됩니다.

afterCompletion은 예외가 발생하더라도 그 정보를 담아 호출됩니다.

인터셉터 - 사용

로그 인터셉터

모든 요청에 로그를 찍는 인터셉터.

package hello.login.web.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@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();

        // LogInterceptor는 싱글톤으로 관리되기 때문에 uuid를 afterCompletion으로 넘길 때 멤버변수로 넘기면 안된다.
        request.setAttribute(LOG_ID, uuid);

        // @RequsetMapping: HanlderMethod
        // 정적 리소스: ResourceHttpRequestHandler
        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler;  // 호출할 핸들러의 모든 메서드 정보가 담겨있다.
        }

        log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
        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();
        String uuid = (String) request.getAttribute(LOG_ID);
        log.info("RESPONSE [{}][{}][{}]", uuid, requestURI, handler);
        if (ex != null) {
            log.error("afterCompletion error!!", ex);
        }
    }
}

인터셉터는 필터와 달리 preHandle, postHandle, afterCompletion 세가지 메서드를 제공하기 때문에 로그를 찍을 때 어떤 요청에서 비롯된 메서드인지 Id를 지정해 줄 필요가 있습니다.
여기서는 UUID임의값을 만들어 넣어주었습니다.

인터셉터는 WAS에서 싱글톤으로 관리되기 때문에 멤버변수를 사용할 수 없습니다.
따라서 UUID정보를 넘길 때 request에 담아 두었습니다,

종료 LOG는 afterCompletion에 담아두었다. 그 이유는 예외가 터지면 postHandle이 호출되지 않기 때문입니다.

handler

핸들러 정보는 어떤 핸들러 매핑을 사용하는가에 따라 달라집니다.

  • HandlerMethod : @Controller, @RequestMapping

  • ResourceHttpRequestHandler : 정적 리소스가 호출 되는 경우.

Log 인터셉터 등록

import hello.login.web.argumentresolver.LoginMemberArgumentResolver;
import hello.login.web.interceptor.LogInterceptor;
import hello.login.web.interceptor.LoginCheckInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;
@Configuration
public class WebConfigInterceptor implements WebMvcConfigurer {

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

    }
}

WebMvcConfigure가 제공하는 addInterceptors를 사용해 인터셉터를 등록할 수 있습니다.

  • registry.addInterceptor(new LogInterceptor()) : 인터셉터 등록

  • order(1) : 인터셉터 체인의 순서를 지정. (낮을수록 빨리 호출)

  • addPathPatterns("/**" ) : 인터셉터를 적용할 URL 패턴 지정,

  • excludePathPatterns("/css/", "/.ico", "/error") : 인터셉터에서 제외할 URL패턴 지정.

서블릿 필터가 제공하는 URL 경로와 다른 패턴이다.

PathPattern 공식 문서.
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html

로그인 인터셉터

package hello.login.web.interceptor;

import hello.login.web.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@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;
    }
}

로그인 유무 체크는 컨트롤러 호출 전에만 확인하면 되기 때문에 preHandle만 구현해 주었다.

로그인 인터셉터 등록

package hello.login;

import hello.login.web.argumentresolver.LoginMemberArgumentResolver;
import hello.login.web.interceptor.LogInterceptor;
import hello.login.web.interceptor.LoginCheckInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebConfigInterceptor 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");
    }
}

비로그인 회원도 홈, 로그인화면등에 접근할 수 있도록 excludePathPatterns를 지정해 주었습니다.


<출처>
Inflearn 김영한 선생님, Spring MVC 1

profile
좋은 사람, 좋은 개발자 (되는중.. :D)

0개의 댓글