📌 요구사항
- 홈 화면 - 로그인 후
- 본인 이름 (OO님 환영합니다.)
- 상품 관리 ⭐
- 로그아웃
요구사항을 다시 살펴보면, 로그인한 사용자만 상품 관리 페이지에 접속할 수 있었다.
하지만 localhost:8080/items
라는 URL만 알면,
로그인 유무와 관계없이 상품 관리 페이지에 접속되는 문제가 발생한다!😣
상품 관리 컨트롤러에서 로그인 여부를 체크하는 로직을 하나하나 작성하여 이 문제를 해결할 수도 있지만, 두 가지 문제점이 있다.
💡 공통 관심사 (
cross-cutting concern
)이렇게 애플리케이션 여러 로직에서 공통으로 관심을 갖는 것을 공통 관심사라고 한다!
공통 관심사는 스프링의 AOP
로도 해결할 수 있지만,
웹과 관련된 공통 관심사는 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다!
→ 웹과 관련된 공통 관심사를 처리할 때 HTTP의 헤더나 URL 정보가 필요한데,
서블릿 필터와 스프링 인터셉터가 HttpServletRequest
를 제공하기 때문이다!
필터는 서블릿이 지원하는 수문장!
HTTP
요청 →WAS
→ 필터 → 서블릿 → 컨트롤러
필터를 적용하면 필터가 호출된 뒤, 서블릿이 호출된다!
→ 모든 고객의 요청 로그를 남기는 요청사항이 있다면 필터를 사용하자!
필터는 특정 URL 패턴에 적용할 수 있으며,
모든 요청에 필터를 적용하고 싶다면 /*
이라고 하면 된다.
- 로그인 사용자
HTTP
요청 →WAS
→ 필터 → 서블릿 → 컨트롤러- 비 로그인 사용자
HTTP
요청 →WAS
→ 필터 (적절하지 않은 요청이라 판단, 서블릿 요청X)
필터에서 적절하지 않은 요청이라고 판단될 경우, 서블릿을 호출하지 않고 끝낼 수도 있다.
→ 로그인 여부 체크하기 좋다😉
HTTP
요청 →WAS
→ 필터1 → 필터2 → 필터3 → 서블릿 → 컨트롤러
필터는 체인으로 구성되어, 중간에 필터를 자유롭게 추가할 수 있다!
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() {}
}
필터 인터페이스를 구현한 뒤 등록하면, 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고 관리한다.
init()
: 필터 초기화 메서드. 서블릿 컨테이너가 생성될 때 호출됨.doFilter()
: 고객의 요청이 올 때마다 해당 메서드 호출됨.destroy()
: 필터 종료 메서드. 서블릿 컨테이너가 종료될 때 호출됨.📌
init
,destroy
는 default 메서드이기 때문에 따로 구현하지 않아도 된다!
가장 단순한 필터인, 모든 요청을 로그로 남기는 필터를 개발해보자!
LogFilter
public class LogFilter implements Filter {}
Filter
인터페이스를 구현해야 한다.doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
doFilter
가 호출된다.ServletRequest request
는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스!String uuid = UUID.randomUUID().toString();
uuid
를 생성해주자.chain.doFilter(request, response);
WebConfig
- 필터 설정@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
스프링 부트를 사용한다면 FilterRegistrationBean
을
사용해서 필터를 등록해주자!
setFilter(new LogFilter())
: 등록할 필터 지정setOrder(1)
: 필터는 체인으로 동작하기 때문에 순서 필요! 낮을수록 먼저 동작한다.addUrlPatterns("/*")
: 필터를 적용할 URL 패턴을 지정. 하나 이상의 패턴 지정 가능!/*
로 지정했기 때문에 모든 요청에 적용됨이번에는 인증 체크 필터를 개발하여,
로그인하지 않은 사용자가 상품 관리 뿐만 아니라 미래에 개발될 페이지에도 접근하지 못하도록 하자!
LoginCheckFilter
- 인증 체크 필터whitelist = {"/", "/members/add", "/login", "/logout","/css/*"};
isLoginCheckPath(requestURI)
requestURI
가 화이트리스트와 일치하는지 검사한다!httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
return;
WebConfig
- loginCheckFilter()
추가@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
setOrder(2)
: 순서를 2로 지정했기 때문에 로그 필터 다음에 로그인 필터가 적용된다.이번에는 스프링 인터셉터에 대해 알아보자!
둘다 웹과 관련된 공통 관심 사항을 처리하지만, 적용되는 순서와 범위, 그리고 사용방법이 다르다
HTTP
요청 →WAS
→ 필터 → 서블릿 → 스프링 인터셉터 → 컨트롤러
HTTP
요청 →WAS
→ 필터 → 서블릿 → 스프링 인터셉터1 → 스프링 인터셉터2 → 컨트롤러
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) throwsException {}
}
단순하게 doFilter()
하나만 제공하는 서블릿 필터와는 달리, 인터셉터는 3단계로 세분화 되어 있다.
- 컨트롤러 호출 전 (
preHandle
)- 호출 후 (
postHandle
)- 요청 완료 이후(
afterCompletion
)
또한, 서블릿 필터는 단순히 request
, response
만 제공했지만, 인터셉터는
handler
)가 호출되는지modelAndView
가 반환되는지도 받을 수 있다.
preHandle
의 응답값이 true
이면 다음으로 진행하고, false
이면 더는 진행하지 않는다.
(false
인 경우 나머지 인터셉터는 물론이고, 핸들러 어댑터도 호출되지 않는다. (1번에서 끝!))
예외가 발생시
preHandle
: 컨트롤러 호출 전에 호출된다.postHandle
: 컨트롤러에서 예외가 발생하면 호출되지 않는다.afterCompletion
: afterCompletion
은 항상 호출된다.afterCompletion()
사용하기!📌 정리
인터셉터는 스프링 MVC 구조에 특화된 필터 기능을 제공한다고 이해하면 된다.
스프링 MVC를 사용하고, 특별히 필터를 꼭 사용해야 하는 상황이 아니라면 인터셉터를 사용하자😃
LogInterceptor
request.setAttribute(LOG_ID, uuid)
preHandler
에서 지정한 값을 postHandler
나 afterCompletion
에서 사용하려면 어딘가에 담아둬야 한다!LogInterceptor
는 싱글톤처럼 사용되기 때문에 멤버 변수를 사용하면 안된다!request
인스턴스에 담아두자!if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler; //호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
}
HandlerMethod
@Controller
, @RequestMapping
을 활용한 핸들러 매핑을 사용하는데, 이 경우에는 핸들러 정보로 HandlerMethod
가 넘어온다.WebConfig
- 인터셉터 등록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")
: 인터셉터에서 제외할 패턴 지정