SpringMVC 2 - 서블릿 필터와 인터셉터
- 서블릿을 호출하기 전에 특정한 처리를 해야할 때 (공통처리) 필터를 사용함
- 여러 로직에서 공통으로 관심 있는 것을 공통 관심사라고 함 (ex 인증)
- 공통 관심사는 AOP를 사용할 수도 있지만, 웹과 관련된 관심사는 필터나 인터셉터를 사용하는 것이 좋다.
(웹과 관련된 부가 기능을 제공해줌!)
* 필터나 인터셉터 구현체를 스프링 빈으로 등록한 뒤에 등록해도됨
만약 필터나 인터셉터 구현체를 만들기 위해 여러 의존객체가 필요하다면 빈으로 등록한 뒤 등록하도록 하자!
1. 서블릿 필터
> HTTP 요청 > WAS > 필터 > 서블릿 > 컨트롤러 순으로 작동함
> 필터에 특정 URL 패턴을 적용할 수 있다.
> 필터는 우선순위를 설정해서 여러개 둘 수 있다. (로그필터 > 로그인 필터)
> 필터 인터페이스는 아래와 같음
public interface Filter {
public default void init(FilterConfig filterConfig) throws ServletException
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;
public default void destroy()
}
> 필터 또한 싱글톤으로 생성 관리됨
1.1 요청 로그 필터
public class LogFilter implements Filter{
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException{
HttpServletRequest req = (HttpServletRequest) request;
String requestURI = req.getRequestURI();
String uuid = UUID.randomUUID().toString();
try{
log.info("Request [{}][{}]", uuid, requestURI);
chain.doFilter(request,response);
}catch(Exception e){
throw e;
}finally{
log.info("Response [{}][{}]",uuid,requestURI);
}
}
}
- uuid 하나의 트랜잭션에 부여한 아이디
- 필터는 HTTP요청이 아닌 경우 까지 고려한 인터페이스 (HTTP 요청에선 request 다운캐스팅해서 사용하자)
- chain.doFilter를 통해 다음 Filter 호출 가능
* 디스팻쳐에서 발생한 에러는 필터를 통과해서 WAS까지 전달됨 중간에 오류처리하면, WAS까지 오류가 못가서 추후에 오류를 보기 위해 지금은 오류가 나면 던지게 함
1.2 필터 설정
@Configuration
public class WebConfig{
@Bean
public FilterRegisterBean logFilter(){
FilterRegisterBean<Filter> fb = new FilterRegisterBean<>();
fb.setFilter(new LogFilter());
fb.setOrder(1);
fb.addUrlPatterns("/*");
return fb;
}
}
- 스프링 부트에서 필터는 위와 같이 설정함 (FilterRegisterBean이용)
- 우선순위를 지정할 수 있음 (URL 패턴의 룰은 서블릿 URL 패턴 룰)
- 어노테이션 기반 필터도 설정 가능하지만, 순서조절이 어렵다 (@WebFilter)
* 같은 요청에 모두 같은 식별자를 자동으로 남기는 방법은 logback mdc를 검색해보자
2. 인증 체크 필터
public class LoginCheckFilter implements Filter {
private static final String[] whitelist = {"/","/members/add","/login","/logout","/css/*"};
public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException{
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
HttpServletResponse httpResponse = (HttpServletResponse) response;
try{
if(isLoginCheckPath(requestURI)){
HttpSession session = httpRequest.getSession(false);
if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null){
//미인증 유저
httpResponse.sendRedirect("/login?redirectURL=" +requestURI);
return; //미인증 사용자는 다음 진행 없이 끝
}
}
chain.doFilter(request,response);
}catch(Exception e){
throw e;
}finally{
//끝
}
}
private boolean isLoginCheckPath(String requestURI){
return !PatternMatchUtils.simpleMatch(whitelist,requestURI);
}
}
- whitelist : 로그인과 관계 없이 접근 할 수 있는 요소지정 (ex css리소스)
- isLoginCheckPath() : 화이트 리스트 제외한 모든 경우 인증 체크 로직 검사 (*PatternMatchUtils 좋다)
- 미인증 사용자는 다시 홈화면으로 리다이렉트 : 근데 로그인 이후에 로그인 전에 접근한 경로를 달아주자
-> 추후 로그인 성공후 redirect 경로를 로그인 전에 시도했던 경로로 보내주자!
- return -> 필터가 더 이상 진행하지 않는다 (서블릿,컨트롤러 호출 x) redirect 후에 요청 종료
2.1 필터 등록
@Bean
public FilterRegisterationBean loginCheck(){
FilterRegistrationBean<Filter> fb = new FilterRegistrationBean<>();
fb.setFilter(); // 동일한 과정
fb.setOrder();
fb.addUrlPatterns();
return fb;
}
2.2 컨트롤러
@PostMapping("/login")
public String loginV4(
@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request{
if(bindingResult.hasErrors()){
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLgoinId(),form.getPassword);
if(loginMember == null){
bindingResult.reject("loginFail","아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
HttpSession session = request.getSession();
session.setAttriute(SessionConst.LOGIN_MEMBER,loginMember);
//redirectURL 적용
return "redirect:" + redirectURL;
}
)
> 필터 덕에 로그인 x 사용자는 특정 경로 외에는 접근 불가 -> 로그인 정책 변경시 조금만 수정하면 됨
*참고로 필터는 request,response 다음 서블릿이나 필터에게 넘겨줄 때 다른 객체로 바꿔서 넘겨줄 수 있다.
(잘 사용하지 않음 참고)
3. 스프링 인터셉터
- 서블릿 필터와 마찬가지로 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기술이다.
(스프링 MVC가 제공하는 기술임)
- 공통 관심사를 처리하지만, 순서와 범위 및 사용법이 다르다.
- 스프링 인터셉터는 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출 됨
(스프링 MVC 기능 -> 서블릿 이후에 등장)
- 인터셉터도 URL 패턴을 적용할 수 있다. 서블릿 URL 패턴과 다름 (매우 정밀한 설정 가능)
- 인터셉터에서 적절하지 않은 요청으로 판단되면, 컨트롤러 호출 없이 끝낼 수 있다 (필터와 비슷)
- 또한 인터셉터도 체인이 자유롭다.
- 필터와 매우 비슷하지만, 더욱 정교하고 다양한 기능 지원함
3.1 인터셉터 인터페이스
- 스프링 인터셉터는 HandletInterceptor 인터페이스를 구현하면 된다.
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 {}
}
- preHandle : 컨트롤러 호출 전 (핸들러 어댑터 호출 전), 위 결과가 true면 다음 진행 false면 진행 안함
나머지 인터셉터와 핸들러 어댑터도 호출하지않음
- postHandle: 컨트롤러 호출 후 (핸들러 어댑터 호출 후)
- afterCompletion : 요청 완료 이후 (뷰가 렌더링 된 이후)
- 인터셉터에는 handler 정보와, modelAndView의 응답 정보도 확인할 수 있다.
3.2 인터셉터 예외 상황
> 예외 발생시 : postHandle 호출 x (컨트롤러 완료x) , afterCompletion은 항상 호출 (ex 파라미터로 예외 볼 수 있음)
* 예외와 무관한 처리는 afterCompletion을 사용하자
3.3 요청 로그 인터셉터
public class LofInterceptor implements HandlerInterceptor{
public static final String LOG_ID = "logId";
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
String requestURI = request.getRequestURI();
String uuid = UUID.randomUUID().toString();
request.setAttribute(LOG_ID, uuid);
if(handler instanceof HandlerMethod){
handlerMethod hm = (HandlerMethod) handler;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse
response, Object handler, ModelAndView modelAndView) throws Exception{
log.info("postHandle [{}]", modelAndView);
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse
response, Object handler, Exception ex) throws Exception {
String requestURI = requestURI();
String logId = (String)request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}]", logId, requestURI);
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
- request.setAttribute(LOG_ID,uuid)
서블릿 필터의 경우 지역변수로 해결이 가능하지만, 스프링 인터셉터는 호출 시점이 완전 분리
따라서 preHandle에서 따로 지정한 값을 다른 메서드로 보내려면 어딘가에 담아야함
싱글톤으로 관리됨 -> 멤버변수는 위험 -> request와 같은 객체 사용하자
*인터셉터 메서드 간 데이터 주고 받을 때는 request에 담아서 주고 받자! 시점이 다르다
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler; //호출할 컨트롤러 메서드의 모든 정보가
포함되어 있다.
}
- handler의 종류를 확인하는 곳임
> HandlerMehod : 어노테이션 기반 (@Controller) 핸들러일 경우 HandlerMehod가 handler로 지정됨
> ResourceHttpRequestHandler: 정적 리소스 호출은 이 핸들러가 지정되서 넘어옴
3.4 인터셉터 등록
@Configuration
public class WebConfig implements WebMvcConfigurer{
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**");
.excludePathPatterns("/css/**","/*.ico","/error");
}
}
- WebMvcConfigurer를 통해 등록할 수 있다.
- 패턴을 매우 세밀하게 지정할 수 있다. (스프링 URL)
4. 인증 체크 인터셉터
public class LoginCheckInterceptor implements HandlerInterceptor{
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)
{
response.sendRedirect("/login?redirectURL=" + requestURI);
return false;
}
return true;
}
}
- 필터에 비해 매우 간결함 컨트롤러 호출 전에만 호출하면 됨
@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"
);
}
//...
}
- addPathPatterns와 exclude만 작성하면 쉽게 사용가능! 또한 세밀하게 패턴 조정 가능하다.
* 필터와 인터셉터는 웹과 관련된 공통 관심사를 해결하기 위한 기술임
필터에 비교해서 인터셉터가 작성하기 훨 편리함 -> 특별한 문제가 없다면 인터셉터 활용하자