로그인 처리 - 필터, 인터셉터(2)

2022. 4. 5. 21:13Spring 기초

로그인 처리 - 필터, 인터셉터(1) 에서 필터를 다루었다.
본 게시물에서는 인터셉터를 다룬다.

스프링 인터셉터도 서블릿 필터와 같이 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기술이다. 서블릿 필터가 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링 MVC가 제공하는 기술이다. 둘다 웹과 관련된 공통 관심 사항을 처리하지만, 적용되는 순서와 범위, 그리고 사용방법이 다르다. 하나씩 살펴보자!

인터셉터의 위치는 디스패처 서블릿과 컨트롤러 사이에 위치해있다. 앞서 배운 필터는 서블릿에서 전처리를 한다면, 인터셉터는 컨트롤러로 날아가는 요청을 낚아채 후딱딱 처리한다음 컨트롤러로 보내는(혹은 보내지 않는) 역할을 수행한다.

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터(적절하지 않은 요청이라 판단, 컨트롤러 호출
X) // 비 로그인 사용자

작동 방식은 필터와 매우 유사하다. 인터셉터에서 걸러내는 방식이다. 인터셉터도 필터처럼 체인형식이라, 여러개를 자유롭게 추가할 수 있다. 

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

여기까지 보면 필터와 인터셉터는 호출 순서만 다르고, 제공하는 기능은 비슷해 보인다. 하지만 인터셉터의 장점은 따로 존재한다. 미리 설명하자면 스프링 인터셉터는 서블릿 필터보다 편리하고, 더 정교하고 다양한 기능을 지원한다.

사용 방법부터 알아보자. filter는 Filter 인터페이스를 구현했다. 인터셉터는 HandlerInterceptor를 구현하면 된다.

public interface HandlerInterceptor {

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

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

서블릿 필터의 경우 단순하게 doFilter() 하나에서 로직을 짜야 했다.

인터셉터는 컨트롤러 호출 전( preHandle ), 호출 후( postHandle ), 요청 완료 이후( afterCompletion )와 같이 단계적으로 잘 세분화 되어 있다. 
또, 서블릿 필터의 경우 단순히 request , response 만 제공했지만, 인터셉터는 어떤 컨트롤러( handler )가 호출되는지 호출 정보도 받을 수 있다. 그리고 어떤 modelAndView 가 반환되는지 응답 정보도 받을 수 있다.
심지어 afterCompletion 메서드의 파라미터로 Exception ex도 넘어오는 것으로 보아 에러 정보도 받을 수 있을것으로 추측된다.

출처: 인프런 김영한 강사님 MVC2

정상 흐름
preHandle : 컨트롤러 호출 전에 호출된다. (더 정확히는 핸들러 어댑터 호출 전에 호출된다.) preHandle 의 응답값이 true 이면 다음으로 진행하고, false 이면 더는 진행하지 않는다. false 인 경우 나머지 인터셉터는 물론이고, 핸들러 어댑터도 호출되지 않는다. 그림 1번에서 끝이 나버린다.
postHandle : 컨트롤러 호출 후에 호출된다. (더 정확히는 핸들러 어댑터 호출 후에 호출된다.)
afterCompletion : 뷰가 렌더링 된 이후에 호출된다.

출처: 인프런 김영한 강사님 MVC2

예외 발생시
preHandle : 컨트롤러 호출 전에 호출된다.
postHandle : 컨트롤러에서 예외가 발생하면 postHandle 은 호출되지 않는다.
afterCompletion : afterCompletion 은 항상 호출된다. 이 경우 예외( ex )를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력할 수 있다. afterCompletion은 예외가 발생해도 호출된다. 예외가 발생하면 postHandle() 는 호출되지 않으므로 예외와 무관하게 공통 처리를 하려면 afterCompletion() 을 사용해야 한다. 예외가 발생하면 afterCompletion() 에 예외 정보( ex )를 포함해서 호출된다.

정리하자면
인터셉터는 스프링 MVC 구조에 특화된 필터 기능을 제공한다고 이해하면 된다. 스프링 MVC를 사용하고, 특별히 필터를 꼭 사용해야 하는 상황이 아니라면 인터셉터를 사용하는 것이 더 편리하다.

다음은 스프링 인터셉터를 이용해 사용자의 요청 로그를 출력하는 코드 예제이다.

@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();  //사용자가 요청한 URI
 	String uuid = UUID.randomUUID().toString();  //랜덤 UUID 받기
 	request.setAttribute(LOG_ID, uuid);
 
     //@RequestMapping: HandlerMethod 핸들러 정보는 어떤 핸들러 매핑을 사용하는가에 따라 달라진다. 
     //스프링을 사용하면 일반적으로
	 //	@Controller , @RequestMapping 을 활용한 핸들러 매핑을 사용하는데, 이 경우 핸들러 정보로
	 //	HandlerMethod 가 넘어온다.
     //정적 리소스: ResourceHttpRequestHandler 

	if (handler instanceof HandlerMethod) {
    	HandlerMethod hm = (HandlerMethod) handler; //호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
 	}
    
 	log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
 	
    return true; //false를 반환하면 이후의 컨트롤러와 인터셉터는 동작하지 않는다.
 }
 
 @Override
 public void postHandle(HttpServletRequest request, HttpServletResponse response, 
 Object handler, ModelAndView modelAndView) throws Exception {
 	log.info("postHandle [{}]", modelAndView);
 }
 
 @Override
 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
 Object handler, Exception ex) throws Exception {
	String requestURI = request.getRequestURI();
 	String logId = (String)request.getAttribute(LOG_ID);
 
 	log.info("RESPONSE [{}][{}]", logId, requestURI);  // 종료 로그.  postHandle은 에러 발생 시 호출 안되므로
    												   // afterCompletion에서 로그를 찍었다.
    if (ex != null) {
 		log.error("afterCompletion error!!", ex);
 	}
 }
}

 

인터셉터의 로직을 구현했다면, 역시나 인터셉터도 등록을 해주어야 한다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

 @Override
     public void addInterceptors(InterceptorRegistry registry) {
         registry.addInterceptor(new LogInterceptor())
         .order(1)
         .addPathPatterns("/**")
         .excludePathPatterns("/css/**", "/*.ico", "/error");
     }
 //...
}
WebMvcConfigurer 가 제공하는 addInterceptors() 를 사용해서 인터셉터를 등록할 수 있다.

registry.addInterceptor(new LogInterceptor()) : 인터셉터를 등록한다.
order(1) : 인터셉터의 호출 순서를 지정한다. 낮을 수록 먼저 호출된다.
addPathPatterns("/**") : 인터셉터를 적용할 URL 패턴을 지정한다.
excludePathPatterns("/css/**", "/*.ico", "/error") : 인터셉터에서 제외할 패턴을 지정한다.

필터와 비교해보면 인터셉터는 addPathPatterns , excludePathPatterns 로 매우 정밀하게 URL 
패턴을 지정할 수 있다

url패턴에 대한 자세한 정보는 아래 링크를 참고하자.

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

 

PathPattern (Spring Framework 5.3.18 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

 

만약 인터셉터로 로그인 여부를 체크하고 싶다면, 컨트롤러 호출 전에 실행되는 preHandle() 메서드만 구현하면 될 것이다.

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

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