매번 보고 또 봐도 까먹고 또 까먹고 헷갈리고 당황하는 것이 이 쪽 친구들인 것 같다.
interceptor, filter, aop, servlet, servlet container, dispatcher servlet...
반복학습을 하면 할수록 점점 마스터할 수 있을 것이라 믿으며..!
필요한 내용에 대해 간략히 정리해 본다.
Servlet container 란?
- 서블렛의 생성부터 소멸까지의 life cycle을 관리
- 요청이 올 때마다 새로운 자바 스레드 생성하고 HttpServletRequest, HttpServletResponse 두 객체를 생성
- 톰캣/제티 등
was와 servlet container의 차이가 궁금해서 찾아봤는데 정확히는 모르겠고 was가 더 큰 개념 같다.
application server > servlet container
interceptor vs filter
필터란(서블릿 필터)
- 필터는 Web Application에 등록되며, 요청 스레드가 Servlet Container에 도착하기 전에 수행(필터가 호출되고 서블릿이 호출)
- 체인으로 구성
- 유일하게 ServletRequest, ServletResponse의 객체를 변환할 수 있다. 주로 전역적으로 처리해야 하는 일에 사용됨
- Springboot에서 특정 urlPattern 에만 필터를 적용하거나 세부적인 설정(우선순위, 필터 이름 등) 시에는 FilterRegistrationBean 빈을 사용하면 되고 아니면 Component로 전역 사용 가능
제공 함수
- init() 메서드: 필터 초기화 메서드이며 서블릿 컨테이너가 생성될 때 호출
- doFilter() 메서드: 고객의 요청이 올 때마다 해당 메서드가 호출되며 메인 로직을 이 메서드에 구현하면 됨
- destory() 메서드: 필터 종료 메서드이며 서블릿 컨테이너가 소멸될 때 호출
사용 예
- 오류 처리
- 인코딩 처리
- 보안 관련 기능
- 데이터 압축이나 변환 기능
- 요청이나 응답에 대한 로그
- 권한 검사
인터셉터란
- 인터셉터는 스프링 콘텍스트에 등록되며, 서블릿 컨테이너를 통과한 후 dispatcher servlet과 컨트롤러 호출 직전/후에 수행됨
- 필터는 서블릿이 제공하는 기술이지만 스프링 인터셉터는 스프링 MVC가 제공하는 기술
- 스프링 컨텍스트 내에 존재하기 때문에 모든 Bean 객체에 접근 가능
- 체인으로 구성
- 필터와 비슷하지만 호출 시점이 다르고 인터셉터가 더 많은 기능을 제공함
제공 함수
- preHandle() 메서드: 컨트롤러 호출 전(HandlerAdapter 호출 전)에 호출. preHandle의 반환 값이 true일 경우 다음 체인으로 진행이 되고, false일 경우 더 이상 진행이 되지 않으며 핸들러 어댑터 또한 호출이 되지 않음
- postHandle() 메서드: 컨트롤러 호출 후(HandlerAdapter 호출 후)에 호출. 컨트롤러의 예외가 발생하면 호출되지 않음
- afterCompletion() 메서드: 뷰가 렌더링 된 이후에 호출되며 컨트롤러의 성공/실패와 무관하게 무조건 호출됨
사용 예
- 보안(spring security)
ContentCachingRequestWrapper
사실 이 모든 것은 애플리케이션의 incoming/outgoing 로깅을 개발하려다가 어디에다 할지, 요즘 추세나 더 좋은 건 없는지 찾아보다가 시작하였다.
필터에서 body까지 확인하기 위해서는 request body를 읽을 수 있어야 하지만 기본적으로 사용되는 HttpServletRequest 같은 경우에는 InputStream은 한 번만 읽어올 수 있고, 두 번째 읽기 시도를 하는 순간 IOException이 발생한다. 그래서 보통 wrapper를 사용하여 구현하는데, 이번에는 아래 두 wrapper를 이용해서 구현해 보았다.
ContentCachingRequestWrapper extends HttpServletRequestWrapper
ContentCachingResponseWrapper extends HttpServletResponseWrapper
주요 로직은 생략하고 request body 가져오는 부분만 보자면 아래와 같다.
//래핑 해제
ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
byte[] buf = wrapper.getContentAsByteArray(); //byte로 본문 가져오기
String body = new String(buf, 0, buf.length, "UTF-8"); //byte to string
response body도 동일한 로직으로 사용하면 된다.
우선 필터를 통해 HttpServletResponse, HttpServletRequest 클래스로 들어온 request와 response를 ContentCachingRequestWrapper와 ContentCachingResponseWrapper로 래핑 해주어야 한다.
왜냐하면 HttpServletRequest 그대로 request.getReader 함수를 호출하거나 안에 있는 데이터를 읽으려고 하면, 단 한 번만 읽을 수 있도록 톰캣에서 만들어두었기 때문에 이걸 다시 읽을 수 있는 클래스로 래핑해주어야 하기 때문이다.
Response도 동일하게, 안에 있는 Body값을 한번만 읽을 수 있게 해 두었기 때문에 필터로 다시 읽을 수 있는 클래스로 래핑 하지 않으면 사용자가 response값을 받지 못하는 참사가 일어날 수 있다.
wrappingResponse.copyBodyToResponse(); 이 부분이 핵심이다. 이걸 통해 body값을 copy 해서 캐시로 저장해 두기 때문에 다시 읽을 수 있다.
https://hirlawldo.tistory.com/44
++ 기존에 web-flux(reactor 기반)만 사용하다가 spring-web을 추가하면서 servlet 기반으로 변경 시
implementation "org.springframework.boot:spring-boot-starter-web"
implementation "org.springframework.boot:spring-boot-starter-webflux"
아래와 같이 두 종류의 api가 있다.
/// mono리턴 -> controller 밖에 단에서 block이 걸려 body 내용물 추출 가능
@GetMapping
public Mono<BaseResponse> getConfig() {
...
return Mono.just(new BaseResponse(result));
}
/// 이미 block되어 내용물이 있는 상태에서 return
@GetMapping("/configuration")
public BaseResponse<MoneyExchangeConfigurationResponse> configuration() {
...
return new BaseResponse<>(moneyExchangeService.getConfiguration());
}
아래와 같이 필터를 걸다간 mono 쪽에서 아무 응답을 받지 못하는 경우가 생긴다.
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
...
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);
chain.doFilter(requestWrapper, responseWrapper); ///
...
}
mono 쪽에서 되려면 reponseWrapper -> reponse로 바꾸면 되긴 하는데..
이러면 body log가 안 남는 문제가 있다.
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
...
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);
chain.doFilter(requestWrapper, response); ///
...
}
이리저리 구글링 해봐도 mono에 대한 body logging은 webFilter를 사용하라고 밖에 안 나오고
여러 방식을 잘 모르고 혼용하는 것은 후에 관리하기 힘들 듯하여 우선은 mono를 들어내고 block 방식으로 할까 한다..
참고: https://www.sollabs.tech/ContentCachingRequestWrapper
로깅 예시 https://vkuzel.com/log-requests-and-responses-including-body-in-spring-boot
'개발 > spring' 카테고리의 다른 글
[spring] filter vs OncePerRequestFilter vs interceptor (0) | 2022.03.02 |
---|---|
[annotation] NotNull NotBlank NonNull NotEmpty... (0) | 2022.02.11 |
[retry] spring-retry test code (1) | 2022.02.07 |
[retry] spring-retry for auto retry (0) | 2022.02.04 |
[spring] graceful shutdown default as of 2.3 (0) | 2022.02.03 |