일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 28 | 29 | 30 | 31 |
- Today
- Total
- 스프링 컨테이너
- qualifier
- 싱글톤 컨테이너
- docker
- 롬복 Qualifier
- Autowired 옵션
- RequiredArgsConstructor
- DI컨테이너
- Servlet Filter
- Spring
- 빈 중복 오류
- 스프링 빈 조회
- ComponentScan
- 도커
- springsecurity
- 라즈베리파이
- HandlerMethodArgumentResolver
- 스프링 빈
- UsernamePasswordAuthenticationFilter
- autowired
- DI
- Spring interceptor
- beandefinition
- 객체지향
- 스프링
- 생성자 주입
- 의존관계 주입
- 라즈베리파이4
- 스프링 Configuration
- 스프링 싱글톤
그날그날 공부기록
필터, 인터셉터 본문
231218 필터, 인터셉터
애플리케이션의 여러 로직에서 공통으로 관심이 있는 것 → 공통 관심사(cross-cutting concern)
ex) 글 작성, 수정, 삭제, 덧글 작성 등의 유저 인증 여부
이런 공통 관심사는 스프링 AOP로도 해결할 수 있지만,
웹과 관련된 공통 관심사는 서블릿 필터 혹은 스프링 인터셉터를 사용하는 것이 좋다고 한다.
서블릿 필터나 스프링 인터셉터는 HttpServletRequest를 제공하기 때문.
서블릿 필터
필터를 사용한 요청의 흐름은 다음과 같다.
만약 적절하지 않은 요청이 올 경우, 필터에서 요청을 종료시켜 서블릿까지 넘기지 않을 수 있다.
Http Request → WAS → Filter → Servlet → Controller
또, 필터는 체인으로 구성되어 여러 필터를 연속적으로 적용할 수 있다.
Http Request → WAS → Filter1 → Filter2 → Filter … → Servlet → Controller
Filter 인터페이스는 다음과 같이 정의되어 있다.
public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {
}
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;
default void destroy() {
}
}
- init()
- 서블릿 컨테이너는 필터를 인스턴스화하고, 한 번만 init메서드 호출.
- doFilter()
- 컨테이너에 의해 호출되며 클라이언트의 요청마다 실행.
- 필터의 로직을 작성하고, chain.doFilter()를 호출해야 함.
- chain에 다음 필터가 있으면 실행, 없으면 서블릿 호출
- destroy()
- 서블릿 컨테이너가 종료될 때 호출
- 필터의 doFilter() 내의 모든 스레드가 종료되거나, timeout 기간이 지난 뒤 호출됨.
간단하게 모든 요청에 로그를 남기는 필터를 작성해 보자.
다음은 Filter를 구현한 LogFilter이다.
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// doFilter 구현
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
서블릿 컨테이너가 생성되고, 종료될 때 init과 destroy가 실행된다.
사용자의 요청마다 동작하는 doFilter는 아래와 같다.
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("log filter doFilter()");
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
// 요청당 임의의 id 부여
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);
}
}
우선 파라미터를 보면 ServletRequest와 ServletResponse를 사용하는데, 이는 HTTP 요청이 아닐 경우까지 고려된 인터페이스이기 때문이다.
다른 요청을 받지 않기 때문에 HttpServletRequest로 다운캐스팅하여 사용한다.
우선, 요청이 Filter에 들어오면 log.info("log filter doFilter()");
가 실행되고,
try문 안의 log.info("REQUEST [{}][{}]", uuid, requestURI);
가 실행된다.
그 위 다른 필터가 있다면 실행하고, 없다면 서블릿이 호출되고 사용자에게 응답을 반환한다.
요청에 대한 응답이 끝나고, 문제가 없었으므로 finally문의 log.info("RESPONSE [{}][{}]", uuid, requestURI);
가 실행된다.
이제 해당 필터를 등록해야 한다.
설정파일을 생성하고, 스프링 부트를 사용해 진행하므로 FilterRegistrationBean을 사용한다.
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
- setFilter(T filter)
- 등록할 필터 지정
- setOrder(int order)
- 필터는 체인으로 동작 → 순서 지정 필요(오름차순으로 실행)
- addUrlPatterns()
- 필터를 적용할 URL패턴 지정
- 내부에서 Set으로 변환되기 때문에 여러 개의 문자열 입력 가능
- 서블릿 사양에 정의된 URL패턴 사용
https://help.perforce.com/hydraexpress/4.3.0/html/rwsfexpservletug/4-3.html
- Logback + MDC 찾아보기
스프링 인터셉터
인터셉터도 필터와 유사하게 공통 관심사를 해결 가능하지만 적용 순서, 범위, 사용법에서 차이가 존재.
서블릿 필터는 서블릿이 기능을 제공하지만, 스프링 인터셉터는 스프링 MVC가 제공하는 기술.
인터셉터의 흐름은 다음과 같다.
스프링 MVC가 제공하는 기능이기 때문에 Dispatcher Servlet 이후, Controller 호출 전에 호출된다.
인터셉터 역시 체인으로 구성되고, 순서도 지정 가능하다.
Http Request → WAS → Filter → Servlet → Inteceptor → Controller
인터셉터는 HandlerInterceptor 인터페이스를 구현하면 된다.
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 {
}
}
- preHandle
- 핸들러가 실행되기 전
- Boolean값을 반환
- true: 실행 체인이 계속 진행됨
- false: 실행 체인에 있는 다른 인터셉터나, 핸들러를 실행하지 않음. (DispatcherServlet이 인터셉터 가체가 요청을 처리했다고 가정)
- postHandle
- 핸들러가 실행된 후
- afterCompletion
- 전체 요청이 완료된 후
인터셉터의 메서드 호출은 다음과 같다.
1. HandlerMapping (핸들러 조회)
2. HandlerAdapter (핸들러 어댑터 조회)
3. *preHandle
4. HandlerAddapter (핸들러 어댑터 실행)
5. *postHandle
6. View 렌더링
7. *afterCompletion
그래서 각 메서드의 파라미터를 보면 Object handler
가 있는 것을 볼 수 있다.
즉, 어떤 컨트롤러(핸들러)가 호출되었는지 정보를 알 수 있다.
또, ModelandView나 Exception도 있어 스프링 MVC 구조에 특화되었다고 볼 수 있다.
만약 적절하지 않은 요청을 처리하기 위해 예외를 발생한다면 preHandle 이후는 실행되지 않는다.
단, afterCompletion은 작동
필터와 동일하게 전체 요청에 로그를 남기는 인터셉터를 작성해 보자.
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
public static final String LOG_ID = "logId";
@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);
// @RequestMapping: HandlerMethod
// 정적 리소스: ResourceHttpRequestHandler
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;// 호출할 컨트롤러 메서드의 모든 정보가 포함되어 있음.
}
log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle [{}]", modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
String requestURI = request.getRequestURI();
String uuid = request.getAttribute(LOG_ID).toString();
log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
- preHandle
- 요청을 구분하기 위한 uuid를 생성
- Filter에서는 try-catch-finally를 사용해 지역변수로 uuid를 유지시키는 것이 가능했다.
- 하지만 인터셉터는 각 메서드의 호출 시점이 분리되어 있어 요청/응답의 uuid값을 같게 표시해 주려면 uuid를 저장해두어야 한다.
- LogInterceptor의 멤버변수로 설정하는 것이 간단해 보이나, 싱글톤처럼 사용되므로 동시성 문제가 발생할 수 있다.
- 따라서 request에 uuid를 저장한 뒤, afterCompletion()에서 꺼내서 사용한다.
- 핸들러 처리
- 핸들러 매핑에서 어떤 핸들러가 선택되었는지에 따라 파라미터로 넘어오는 값이 달라진다.
- Spring에서는 일반적으로 @Controller, @RequestMapping을 사용하는데,
이 경우에는 HandlerMethod가 넘어온다. - 컨트롤러가 아닌, 정적 리소스를 호출하는 경우 ResourceHttpRequestHandler가 넘어온다.
- 요청을 구분하기 위한 uuid를 생성
@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);
// @RequestMapping: HandlerMethod
// 정적 리소스: ResourceHttpRequestHandler
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;// 호출할 컨트롤러 메서드의 모든 정보가 포함되어 있음.
}
log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
return true;
}
- postHandle
- 예외가 발생하면 호출되지 않는다.
- afterCompletion
- 예외가 발생해도 실행이 보장된다.
인터셉터 역시 Config파일에 등록해 주어야 작동한다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error");
}
}
- Config파일에 WebMvcConfigure의 addInterceptors를 사용하면 등록 가능하다.
- registry.addInterceptor(): 인터셉터를 등록한다.
- order(): 호출 순서를 지정한다.(오름차순으로 호출)
- addPathPatterns(): 인터셉터를 적용할 URL패턴을 지정
- excludePathPatterns(): 인터셉터 실행을 제외할 패턴 지정
인터셉터에서는 스프링이 제공하는 URL 형식을 사용하기 때문에 인지하고 있어야 한다.
(필터에서 사용되는 서블릿 URL과는 다르다.)
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html
'Spring 공부' 카테고리의 다른 글
ArgumentResolver (0) | 2023.12.19 |
---|---|
쿠키, 세션 사용해보기 (0) | 2023.11.28 |
스프링 시큐리티 로그인 동작 따라가보기 (2) | 2023.09.25 |
빈 생명주기, 콜백 이용하기 (0) | 2022.08.16 |
Map, List에 빈 주입 & 사용하기 (0) | 2022.08.09 |