회사에서는 관리자 페이지의 보안을 OAuth2 방식으로 하고 있습니다. 유효한 토큰을 API에서 받아와 세선에 저장해놓고 request를 보낼 때 마다 세션에서 토큰이 유효한지 확인하는 작업을 하고 있습니다. 토큰은 클라이언트에서 쿠키로 관리하고 있으며 매 요청을 보낼 때마다 쿠키의 토큰을 확인하고 있습니다. 매 요청마다 session을 찾아 유효한 토큰이 있는지 살피다 보니 매우 반복적인 작업이 되었습니다. React를 사용하면서 Axios
의 Interceptor
기능이 있었는데 서블릿에서 기본적으로 Interceptor
를 제공해주고 있습니다. 이 방식은 매 요청이 이루어질 때마다 해야되는 작업이므로 수평적으로 관리해주어야 할 필요가 있습니다. 대표적인 방법들이 Filter
, Interceptor
, AOP
가 있습니다. 그중에서 오늘은 Interceptor
를 어떻게 사용하는지 살펴보고 토큰을 가져오는 방법에 대해서 살펴보았습니다.
1. Interceptor
농구에서 인터셉터
라는 용어가 있습니다. 상대방의 공을 훔친다 라는 의미로 사용됩니다.
서블릿에서는 request를 훔치다 라는 의미로 사용됩니다. request가 controller로 들어가기 전과 후에 어떠한 작업을 해주고 싶을 때 interceptor
를 사용합니다.
이 intercpetor
에 대해서 이해하려면 먼저 HandlerMapping
과 HandlerAdaptor
를 이해해야 합니다.
HandlerMapping과 HandlerAdaptor
HandlerMapping
은 간단히 request의 URL과 매칭되는 Controller를 선택해주는 역할을 합니다.HandlerAdaptor
는 Controller의 메소드를 실행해주는 역할을 합니다.
추가적으로 DispatcherServlet
은 중앙의 Servlet으로 request를 Controller로 전달하는 역할을 합니다. 적절한 controller를 선택하는 작업을 HandlerMapping
에게 전달합니다. 그리고 선택된 Controller를 실행하는 작업을 HandlerAdaptor
에게 전달합니다.
아래의 그립을 보겠습니다.
순서대로 보면 /login
이라는 request를 처리할 때, DispatcherServlet
이 HandlerMapping
을 통해 /login
과 매칭되는 적절한 Controller를 찾습니다. 그리고 DispatcherServlet
이 HandlerAdaptor
를 사용하여 찾은 Controller의 메소드를 실행하게 됩니다. 여기서 interceptor
의 개념이 사용됩니다. 즉, Interceptor
는 찾은 Controller
의 실행 전후에 어떠한 처리를 위해서 사용하게 됩니다.
아래는 조금 더 상세한 흐름을 설명한 것입니다. 참고로 보시기 바랍니다.
- DispatcherServlet이 request를 받는다
- DispatcherServlet은 Controller를 선택하는 일을
HandlerMapping
에게 전달. 선택된 Controller를 DispatcherServlet에 다시 전달 - DispatcherServlet은 Controller의 비즈니스 로직을 실행하는 작업을
**HandlerAdapter**
에게 전달 - HandlerAdapter가 Controller의 비즈니스 처리 로직 호출
- Controller가 비즈니스 로직 실행하고 결과를 Model을 HandlerAdapter에게 전달
- DispatcherServlet이 view를 처리하는 일을 view이름과 함께
ViewResolver
에게 전달 - ViewResolver는 view이름과 매칭되는 View를 리턴
- DispatcherServlet은 rendering 프로세스를 리턴받은 view에 적용
- view는 Model과 response를 render한다
Handler Interceptor
자바에서는 Handler Interceptor
라는 인터페이스가 interceptor
의 역할을 합니다.
여기서 handler
라는 단어가 있는데 여기서는 Controller
의 역할로 사용되게 됩니다. 자세한 내용은 이곳을 참조하시기 바랍니다. 통상적으로 handler
는 Controller
를 포함한 HttpRequestHandler
, WebRequestHandler
등과 같이 Dispatcher Servlet
과 함께 일하는 것들을 통칭합니다. Controller
는 Dispatcher Servlet
과 협업하여 web request
를 실행하고 view
를 반환하기 때문에 handler
라고 불려집니다.
handler
사이에서 어려가지 역할을 수행하는데 다음과 같이 여러번 반복되는 handler
코드를 처리할 수 있습니다
- logging
- model의 전역적으로 사용되는 파라미터 변경
- session 관리
- 권한 관리
2. Interceptor 처리 과정
앞서 말씀드렸다시피 Interceptor는 총 3개의 메소드를 가지고 있습니다.
- prehandle : Controller 동작 이전
- posthandle : Controller 동작 이후, View 동작 이전
- afterCompletion : View 동작 이후
각각의 동작들은 요청이 들어온 이후 순서대로 진행이 됩니다. prehandle과 posthandle은 Controller 동작 이전과 이후로 나눠지고 afterCompletion은 View가 동작한 이후 실행 됩니다.
아래의 그림을 봐주시기 바랍니다.
- 먼저
/test
라는 요청이 들어오게 됩니다.- 이때 DispatcherServlet이 HandlerMapping을 통해
/test
라는 요청에 해당하는 Controller를 선택하도록 시킵니다.
- 이때 DispatcherServlet이 HandlerMapping을 통해
Interceptor
의 prehandle 메소드가/test
요청을 Controller가 동작하기 이전에 가로챕니다.- Controller가 실행됩니다
- DispatcherServlet이 HandlerAdaptor를 통해
/test
라는 요청에 해당하는 Controller를 실행하도록 시킵니다.
- DispatcherServlet이 HandlerAdaptor를 통해
Interceptor
의 posthandle 메소드가 Controller가 실행된 이후 동작합니다.- posthandle 메소드가 실행되고 View가 실행되어 response를 클라이언트에게 전달합니다.
interceptor
의 afterCompletion 메소드가 View가 실행된 이후에 동작합니다.
3. Interceptor 생성 방법
인터셉터를 생성하는 방법은 두가지가 있습니다.
- HandlerInterceptorAdaptor
- HandlerInterceptor
1. HandlerInterceptor
HandlerInterceptor
는 Interceptor를 관리하는 인터페이스입니다.
이 인터페이스는 총 3개의 메소드를 가지고 있습니다.
- prehandle
- posthandle
- afterCompletion
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
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 {
}
}
2. HandlerInterceptorAdaptor
HandlerInterceptorAdaptor
는 HandlerInterceptor
를 상속받은 추상 클래스로 간편하게 사용할 수 있습니다. HandlerInterceptor
와 같이 preHandle
, postHandle
, afterComplete
세가지 메소드를 Override하여 사용할 수 있습니다. 추가적으로 비동기 처리를 함께 할 수 있습니다
public abstract class HandlerInterceptorAdapter implements AsyncHandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
@Override
public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
}
}
Interceptor 사용 방법
1. Interceptor 생성
@Component
public class MyInterceptor implements HandlerInterceptor {
// 1. Controller 보내기 전
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandler");
System.out.println(handler);
// false이면 controller로 요청 안한다
return true;
}
// 2. Controller의 handler처리 후
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandler");
System.out.println(handler);
System.out.println(modelAndView);
}
// 3. View 처리 이후
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion");
System.out.println(handler);
}
}
2. Interceptor 등록
@Configuration
@RequiredArgsConstructor
public class MyWebMvcConfigurer implements WebMvcConfigurer {
private final HandlerInterceptor handlerInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(handlerInterceptor)
.addPathPatterns("/**");
}
}
테스트로 확인
@AutoConfigureMockMvc
@SpringBootTest
class HelloControllerTest {
@Autowired
private MockMvc mvc;
@Test
void hello() throws Exception {
mvc.perform(get("/"))
.andExpect(status().isOk());
}
}