쇼핑몰을 예를 들어 보자면, 주문 현황 페이지는 게스트가 아닌 회원만이 접속할 수 있어야 한다.
만약 게스트가 주문 현황 페이지에 접근하게 되면 타 페이지로 리다이렉트되야 할 것이다
하지만 위와 같이 회원만이 접근할 수 있는 페이지가 여러 개일 경우 일일이 컨트롤러에서 로그인 여부를 체크해서 리다이렉션해주어야 할까?😅
모든 컨트롤러에서 매번 권한을 체크하고 리다이렉션해주어야 한다면 👎실수가 발생할 확률이 높아질 것이고, 👎로직이 변경/추가될 때 일일이 변경해줘야 하는 수고가 생긴다.
이처럼 여러 곳에 걸쳐서 공통의 관심사가 적용되는 경우를 '공통 관심사(Cross-cutting Concern)'라고 한다.
위 쇼핑몰과 같은 예시는 인증에 대한 공통 관심사가 존재한다고 말할 수 있겠다.
공통... 관심사? AOP로도 해결 가능하겠지만 웹 관련 공통 관심사는 주로 서블릿 필터 / 스프링 인터셉터를 사용한다.
서블릿 필터
필터는 마치 DispatcherServlet과 같이 수문장 역할을 한다 😊
필터 플로우
HTTP 요청 👉 WAS 👉 필터(1번째, 2번째, 3번째) 👉 서블릿 👉 컨트롤러
정말 정수기의 필터처럼... 잘못된 호출의 경우 필터에서 걸러져 서블릿이 호출되지 않는다.
여러 필터를 체인처럼 사용할 수도 있다. 그리고 필터는 싱글톤으로 생성됨을 주의하자
필터 구현(+생명주기)
init() 👉doFilter() 👉 destroy()
init()
config 객체를 저장한다. 필터 객체를 초기화하고 서비스에 추가하기 위한 메소드이다.
웹 컨테이너가 1회 init 메소드를 호출하여 필터 객체를 초기화하면 이후의 요청들은 doFilter를 통해 처리된다.
doFilter()
doFilter 메소드는 url-pattern에 맞는 모든 HTTP 요청이 디스패처 서블릿으로 전달되기 전에 웹 컨테이너에 의해 실행되는 메소드이다.
doFilter의 파라미터로는 FilterChain이 있는데, FilterChain의 doFilter 통해 다음 대상으로 요청을 전달하게 된다.
chain.doFilter() 전/후에 우리가 필요한 처리 과정을 넣어줌으로써 원하는 처리를 진행할 수 있다.
dofilter 내에서 dofilter()를 실행해 주어야 다음 단계(필터 혹은 서블릿)로 넘어간다!
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("log filter dofilter");
HttpServletRequest httpRequest = (HttpServletRequest) request;
// HttpServletRequest를 이용 시 다운캐스팅 해주기
chain.doFilter(request, response);
}
destroy()
destroy 메소드는 필터 객체를 서비스에서 제거하고 사용하는 자원을 반환하기 위한 메소드이다.
이는 웹 컨테이너에 의해 1번 호출되며 이후에는 이제 doFilter에 의해 처리되지 않는다.
필터의 등록
앞서 구현한 Filter를 등록해 보자😊
스프링 부트 사용시 FilterRegistrationBean을 사용한다
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter(){
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter()); // 등록 필터 지정
filterRegistrationBean.setOrder(1); // 필터 순서
filterRegistrationBean.addUrlPatterns("/*"); // 필터 적용 URL 패턴
return filterRegistrationBean;
}
}
위와 같이 필터를 등록하면 필터가 적용된 URL 패턴에서는 필터가 동작하는 것을 확인할 수 있다.
스프링 인터셉터
서블릿이 아닌 MVC가 제공하는 기술
HTTP 요청 👉 WAS 👉 필터 👉 디스패처 서블릿 👉 스프링 인터셉터 👉 컨트롤러
적절치 못한 요청이 들어오면 컨트롤러를 호출하지 않는다
인터셉터는 필터보다 정교하고, 편리하다
인터셉터 구현
HandlerInterceptor를 구현하면 된다.
public class LogInterceptor implements HandlerInterceptor {
preHandle()
컨트롤러 호출 전에 호출
@Override
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;// 호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
}
이 때, handler의 종류에 따라 분기를 나누어 처리할 수 있음을 기억하자
정적 리소스가 호출될 경우 👉 ResouceHttpRequestHandler
@Controller, @RequestMapping을 사용한 핸들러 매핑의 경우 👉 HandlerMethod
postHandle()
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
컨트롤러에서 예외가 발생하지 않을 시 호출
afterCompletion()
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
String requestURI = request.getRequestURI(); // preHandle에서 이용한 값 꺼내기
String uuid = (String) request.getAttribute(LOG_ID);
예외가 발생하든, 발생하지 않든 완료 후 항상 호출
preHandle에서 사용한 값을 postHandle, afterCompletion에서 사용하고자 한다면 HttpServletRequest에 담아서 이용하자.
HttpServletRequest에 대해 아래 내용을 복기🙃
HttpServletRequest는 HTTP 요청이 시작부터 끝날 때까지 유지되는 임시 저장소의 특징을 가진다
preHandle에서 setAttribute를 통해 key, value 형식으로 정보를 저장하고
request.setAttribute(LOG_ID,uuid);
afterCompletion에서 getAttribute를 통해 정보를 가져오자
String uuid = (String) request.getAttribute(LOG_ID);
구현한 인터셉터 등록
WebMvcConfigurer - addInterceptors를 구현
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor()) // 특정 인터셉터 등록
.order(1) // 인터셉터 순서
.addPathPatterns("/**") // 인터셉터를 적용할 패턴
.excludePathPatterns("/css/*", "/*.ico", "/error"); // 인터셉터를 적용하지 않을 패턴
}
인터셉터는 화이트리스트 같은 것을 코드로 구현하지 않고, 위와 같이 .excludePathPatterns()를 이용하면 된다.
앞단의 로직만을 관리하며 인터셉터 적용 패턴 부분이 간단하다는 면에서 인터셉터의 구현이 필터보다는 용이함을 알 수 있다.
ArgumentResolver
컨트롤러의 파라미터에 대해 ArgumentResolver를 적용해보자
아래의 Member 변수가 타깃🔍
@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {
애노테이션 생성
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
@Target(ElementType.PARAMETER) : 파라미터 타입에 적용
@Retention(RetentionPolicy.RUNTIME) : 런타임까지 애노테이션 정보 유지
ArgumentResolver 구현
HandlerMethodArgumentResolver를 상속받아 구현
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
supportsParameter
1️⃣ 애노테이션이 붙어 있는지
2️⃣ 적용하고자 하는 타입이 맞는지 확인한다
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class); // 애노테이션이 붙어있는지 확인
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType()); // 타입 확인
return hasLoginAnnotation && hasMemberType;
}
resolveArgument
컨트롤러 호출 직전에 호출 되어 파라미터 정보 생성
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
log.info("resolveArgument 실행");
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); // HttpServletReqeust로 변환
HttpSession httpSession = request.getSession(false);
if (httpSession == null) { // 세션이 없다면 null 반환 -> loginMember가 없다
return null;
}
return httpSession.getAttribute(SessionConst.LOGIN_MEMBER);
}
ArgumentResolver 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
'Spring' 카테고리의 다른 글
Spring Cloud Config (0) | 2024.10.30 |
---|---|
Spring - Exception (0) | 2022.10.09 |
MVC2 - 로그인(쿠키, 세션) (1) | 2022.10.03 |
MVC2 - Bean Validation (0) | 2022.09.27 |
MVC2 - Validation (0) | 2022.09.27 |