Spring Boot에서 Interceptor 사용하기

인터셉터?

Spring 또는 Spring Boot 프레임워크로 웹 어플리케이션을 개발하다 보면 아래와 같은 요구사항이 생기게 됩니다.

  1. 특정 url 진입 시 로그인이 된(인가된) 사용자가 접근을 해야 함
  2. 특정 url 진입 시 Jwt와 같은 토큰을 검사해야 함
  3. 특정 url의 경우 계정의 권한에 따라 접근을 막아야 함 (물론 이 부분은 view단에서 처리할 수도 있습니다.)

위 요구사항의 특징은 특정 url 진입 시 어떤 작업을 수행해야 하는 것 입니다.
이름의 의미와 같이 무언가를 진행할 때 특정 작업을 수행하는 것이 인터셉터입니다.
Java 웹 프로그래밍에서는 이런 작업을 하는 기술로는 Servlet Filter, Interceptor, AOP가 있습니다.

이 세가지 기술에 대한 차이를 먼저 간략하게 살펴보고 포스팅을 진행하도록 하겠습니다.
(AOP의 경우 서블릿 필터와 인터셉터와 성향이 많이 다르기에 AOP는 향후 별도 포스팅에서 다루겠습니다.)


인터셉터와 서블릿 필터의 차이점

먼저 설명에 앞서 사진을 하나 보면서 설명 드리겠습니다.

위 사진은 Spring request lifecycle을 도식화 한 것입니다.
사진에서 앞 사용자의 요청에 보면 필터(Filter)와 디스패쳐 서블릿이 보일겁니다.

Servlet Filter는 Dispatcher Servlet의 앞단에서 들어오는 요청 정보를 처리합니다.
Interceptor는 Dispatcher Servlet의 뒤의 Handler 영역에서 요청 정보를 처리합니다.
그리고 Servlet Filter의 경우 J2EE의 표준 스펙에 정의가 되어있습니다만, Interceptor는 Spring 프레임워크에서 자체적으로 제공하는 기능입니다.
또한 Servlet Filter는 디스패처 서블릿 전에 호출이 되므로 Spring 프레임워크와 무관한 것을 처리할 수 있습니다.

정리하여 Spring에서 흐름 순서를 보자면 아래와 같습니다.

사용자 요청(Request) -> Servlet Filter -> Dispatcher Servlet -> Interceptor -> Controller

둘의 차이점에 설명은 이것으로 마무리하고, Spring Boot환경에서 인터셉터를 적용하는 방법에 대하여 알아보도록 하겠습니다.


Spring Boot에서 Interceptor 적용하기

이번 예시의 환경은 다음과 같습니다.

  • Spring Boot 1.5.8
  • Java Config

Spring Boot에서 적용하려면 다음의 과정을 거쳐서 적용합니다.


1. HandlerInterceptorAdapter를 상속받은 클래스 구현

인터셉터 구현을 하기 위해서는 HandlerInterceptorAdapter 추상 클래스를 상속받아 구현해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
public class TestInterceptor extends HandlerInterceptorAdapter {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("Interceptor > preHandle");
return true;
}

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

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object object, Exception arg3) throws Exception {
log.info("Interceptor > afterCompletion" );
}
}

HandlerInterceptorAdapter 추상 클래스를 상속받아 구현합니다.
저 추상 클래스에는 아래와 같은 메서드가 선언되어 있고 다음과 같은 작업을 수행합니다.

  • PreHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    • 컨트롤러(즉 RequestMapping이 선언된 메서드 진입) 실행 직전에 동작.
    • 반환 값이 true일 경우 정상적으로 진행이 되고, false일 경우 실행이 멈춥니다.(컨트롤러 진입을 하지 않음)
    • 전달인자 중 Object handler핸들러 매핑이 찾은 컨트롤러 클래스 객체입니다.
  • PostHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
    • 컨트롤러 진입 후 view가 랜더링 되기 전 수행이 됩니다.
    • 전달인자의 modelAndView을 통해 화면 단에 들어가는 데이터 등의 조작이 가능합니다.
  • afterComplete(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
    • 컨트롤러 진입 후 view가 정상적으로 랜더링 된 후 제일 마지막에 실행이 되는 메서드입니다.
  • afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler)
    • Servlet 3.0부터 비동기 요청이 가능해짐에 따라 비동기 요청 시 PostHandleafterCompletion메서드를 수행하지 않고 이 메서드를 수행하게 됩니다. (Spring에서 제공함)

저 같은 경우 진행하는 프로젝트에서 비동기는 사용하지 않아서 afterConcurrentHandlingStarted 메서드를 구현하지 않았지만, 비동기를 사용하게 된다면 해당 메서드를 적절하게 구현하여 사용하면 될 것 같습니다.


2. WebMvcConfigurerAdapter를 상속받은 설정 클래스 구현

저 같은 경우 대다수 설정을 java로 구현을 하여, 아래와 같이 WebMvcConfigurerAdapter를 상속받아 설정을 하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.mhlab.cb.component.interceptors.SessionCheckInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TestInterceptor())
.addPathPatterns("/*")
.excludePathPatterns("/test/**/")
.excludePathPatterns("/users/login"); //로그인 쪽은 예외처리를 한다.
}
}

addInterceptors 메서드를 구현을 할 때 위와 같이 구현을 하며, 인터셉터 구현 시 메서드 체이닝이 되어 있는데 이는 다음과 같습니다.
addInterceptor는 등록할 인터셉터를 설정하며, addPathPatterns는 적용할 url 패턴을 설정합니다.
예시처럼 /* 로 표시할 경우 모든 url에 대하여 인터셉터를 호출하게 됩니다.
또한 체이닝을 이용하여 연속적으로 다른 추가 패턴도 등록할 수 있습니다.
excludePathPatterns의 경우 인터셉터를 제외할 url 패턴을 등록하는 메서드로써 해당 url로 접근 시에는 인터셉터를 적용하지 않게 됩니다.


정리

위 내용을 통해 인터셉터에 대해 간략하게 알아보았습니다.
이를 응용한 것은 12월 4일 연구일지 포스팅에서 SessionCheckInterceptor를 통해서 Jwt 검증을 하는 것에서 볼 수 있었습니다.
저 포스트에서는 상세하게는 구현되어있지 않지만 코드를 보면 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("Interceptor > preHandle");
if(hasSessionInAccount(request.getSession())) { return true; }//세션에 계정 정보가 존재하는 경우
else {
try { response.sendRedirect("/users/login/nosession"); }
catch (IOException ie ) {} //만약 리다이렉션 도중 에러가 난 경우
return false;
} //세션 정보가 존재하지 않는 경우
}

이처럼 인터셉터를 활용한다면 유용하게 사용할 수 있습니다.
다음 포스팅에서는 AOP에 대하여 포스팅을 진행해보도록 하겠습니다.

포스팅을 읽어주셔서 감사합니다.