
기본적으로 요청이 들어오면 필터를 가장 먼저 거쳐가게 된다. 필터를 거친 후 디스패처 서블릿을 지나고, 핸들러 인터셉터를 지나고, 컨트롤러로 내용이 들어온다. 필터는 톰캣이 관리하는 웹 컨텍스트에서 작동하며, 스프링의 관리를 받지 않는다. 반면 인터셉터는 스프링에서 관리한다.
최전방에 있기 때문에 필터는 클라이언트에서 전달된 가장 날 것의 리퀘스트를 받는다. 때문에 필터는 들어온 데이터를 다른 데이터로 변환시켜 안쪽으로 보낸다든지, 들어온 데이터에 대해 내용을 기록하든지 로그 시스템을 많이 활용한다.
인터셉터는 스프링에서 관리하는 빈으로, 컨트롤러로 보내기 바로 전 단계를 맡고 있다. 이미 핸들러 맵핑을 통해 어느 컨트롤러로 보낼지 요청의 방향이 정해져있기 때문에, 해당 컨트롤러가 가진 애너테이션을 찾아 작업을 한다든지, 헤더의 내용을 참고하여 권한을 검사하든지 등 주로 인증 작업을 맡는다. 정상적인 데이터가 전달되었는지, 해당 주소로 보내는 이 컨트롤러에 어떤 권한을 가지고 있는지 확인하여 승인하거나 반려하는 등의 역할을 하는 것이다.
다음과 같은 UserRequest를 POST 방식으로 보낼 때 들어오는 값을 필터를 사용해 출력해보자.
public class UserRequest {
private String name;
private String phoneNumber;
private String email;
private Integer age;
----
@PostMapping("")
public void register(
@RequestBody UserRequest userRequest
){
log.info("{}", userRequest);
}
만약 json을 보낼 때 name이 아닌 username에 담아 보내고, phone_number가 아닌 phoneNumber에 담아 보내면 코드는 200번대가 나오지만 실제로 로그에는 name과 phoneNumber가 null로 나온다.
이를 HttpEntity로 바꿔서 받아보면 어떤 값이 들어왔는지 null로 처리되지 않고 그대로 출력되지만, 이 경우에는 HttpEntity를 한번 더 캐스팅해주어야 해서 번거롭다.
@PostMapping("")
public void register(
HttpEntity http
){
log.info("{}", http.getBody());
}
UserRequest를 그대로 반환하게 해도 이것이 결국 어떻게 전달되어 controller로 넘어갔는지 알 방법이 없다. 이를 확인하기 위해 필터를 사용해본다.
@Component
@Slf4j
public class LoggerFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 진입 전
log.info(">>>> 진입");
chain.doFilter(request, response);
// 진입 후
log.info("<<<< 리턴");
}
}
필터를 만들려면 javax(혹은 jakarta)의 servlet에서 filter를 상속하여 doFilter를 오버라이드해주어야 한다.
(init은 가장 처음 실행시에, destroy는 종료될 때 실행되는 메서드들이다. 이 둘은 따로 건드리지 않았다.)
이를 실행하고 다시 api에 요청을 보내보면 필터에서 로그가 잘 찍히는 것을 볼 수 있다.
이처럼 필터에서는 컨트롤러로 진입 전, 후에 로그를 찍을 수 있고, request와 response에 따로 조작할 수 있다.
var req = new HttpServletRequestWrapper((HttpServletRequest) request);
var res = new HttpServletResponseWrapper((HttpServletResponse) response);
var br = req.getReader();
var list = br.lines().toList();
list.forEach(it -> {
log.info("{}",it);
});
request와 response를 HttpServletRequest와 Response 형태로 변환하여 BufferedReader로 들어온 내용을 한줄씩 읽어오게 할 수 있다. 그러나 이렇게 필터에서 요청 내용을 읽어버리면, 컨트롤러에서 더 읽어올 내용이 없어서 오류가 난다. 하여 ContentCachingWrapper를 사용해 request와 response를 읽고 그 내용을 저장하도록 한다. 이 Wrapper는 내용을 읽어올 때 내부에 contents caching byte array에 해당 내용을 담아두기 때문에 읽은 후에도 다시 읽을 수 있다.
// 진입 전
log.info(">>>> 진입");
var req = new ContentCachingRequestWrapper((HttpServletRequest) request);
var res = new ContentCachingResponseWrapper((HttpServletResponse) response);
chain.doFilter(request, response);
var reqJson = new String(req.getContentAsByteArray());
log.info("req: {}", reqJson);
var resJson = new String(res.getContentAsByteArray());
log.info("res: {}", resJson);
// 진입 후
log.info("<<<< 리턴");
res.copyBodyToResponse();
CachingWrapper를 통해 ByteArray로 내용을 저장하게 하고, 이를 String으로 변환하여 로그에 찍어보면 들어온 내용과 controller에서 반환한 응답이 찍힌다. 추가로, 이 ByteArray를 읽어버리면 응답으로 읽어 보낼 내용이 또 비어버리기 때문에 맨 마지막에 response의 body를 복사해주어야 응답이 제대로 반환된다.
이렇게 필터는 들어오고 나가는 원본 데이터를 확인하는 로깅으로 활용할 수 있다.
동일한 프로젝트에 인터셉터를 추가해본다. 인터셉터는 HandlerInterceptor를 상속받는다. preHandle은 controller로 전달되기 전이고, postHandle은 화면과 뷰가 연결되었을 때, afterCompletion은 완료되었을 때 호출된다.
@Slf4j
@Component
public class OpenApiInterceptor implements HandlerInterceptor {
// controller로 전달, false면 전달x
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler)
throws Exception {
log.info("pre handle");
return true;
}
// 화면에 뷰가 연결 되었을 때 호출됨
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
@Nullable ModelAndView modelAndView)
throws Exception {
log.info("post handle");
}
// 완료됐을 때 호출
// 예외도 함께 넘어옴.
// 어떤 세션이 발생했고 어떤 핸들러가 있었고 등
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
@Nullable Exception ex)
throws Exception {
log.info("after completion");
}
}
인터셉터가 작동하려면 추가로 WebConfig를 작성해야 한다. WebMvcConfigurer를 상속하며, addInterceptors로 인터셉터를 등록해준다.
인터셉터를 여러 개 등록하는 경우, 등록한 순서대로 동작하게 된다. 혹은 order로 값을 부여하여 높은 값을 가진 인터셉터가 먼저 실행되도록 할 수도 있다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private OpenApiInterceptor openApiInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry){
// root 하위의 모든 주소를 전부 맵핑하여 openApiInterceptor를 적용하겠다는 뜻
registry.addInterceptor(openApiInterceptor)
.addPathPatterns("/**");
}
}
서버를 켜서 똑같은 요청을 보내면, 필터와 인터셉터가 작동하는 것을 볼 수 있다.
- 파란색이 filter, 빨간색이 interceptor이다.
- 필터가 먼저 실행되고, 인터셉터는 preHandle, postHandle, afterCompletion 순으로 실행되는 것을 볼 수 있다.
먼저 핸들러를 핸들러메서드로 형변환시켜준다.
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler)
throws Exception {
// log.info("pre handle");
var handlerMethod = (HandlerMethod)handler;
return true;
}
다음으로 핸들러 메서드의 메서드 레벨과 클래스 레벨을 확인할 수 있도록 커스텀한 애터네이션을 하나 만든다.
@Target(value = {ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenApi {
}
컨트롤러로 넘어가서 Mapping을 하나 더 추가해주고, 기존의 post 맵핑된 url에는 @OpenApi를 붙여 두 맵핑의 차이를 만들어준다.
@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserApiController {
@OpenApi
@PostMapping("")
public UserRequest register(
@RequestBody UserRequest userRequest
){
log.info("{}", userRequest);
return userRequest;
}
@GetMapping("/hello")
public void hello(){
log.info("hello");
}
}
다시 인터셉터로 돌아가서 메서드를 완성해준다.
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler)
throws Exception {
// log.info("pre handle");
var handlerMethod = (HandlerMethod)handler;
var methodLevel = handlerMethod.getMethodAnnotation(OpenApi.class);
if (methodLevel != null){
log.info("method level");
return true;
}
var classLevel = handlerMethod.getBeanType().getAnnotation(OpenApi.class);
if (classLevel != null){
log.info("class level");
return true;
}
log.info("not open api: {}", request.getRequestURI());
return false;
}
preHandle이 파라미터로 받는 handler를 HandlerMethod로 형변환하고, 이 handlerMethod가 가진 메서드 중 특정 애너테이션이 적용되어있는지 확인하는 메서드를 사용한다. @OpenApi 애너테이션이 메서드에 적용되어있거나 클래스에 적용되어있으면 인터셉터가 작동 및 컨트롤러로 요청을 넘기며, 없으면 주소를 로그에 찍고 컨트롤러로 요청을 보내지 않도록 한다.
다시 서버를 켜서 /api/user로 post 요청을 보내보면,
먼저 필터가 적용된 다음, 인터셉터에서 메서드에 적용된 @OpenApi 애너테이션을 찾아 컨트롤러로 내용이 전달된 것을 확인할 수 있다.
반면 /api/user/hello로 get 요청을 보낼 시,
필터는 작동하였으나 @OpenApi 애너테이션이 메서드나 클래스 레벨에 적용된 것이 없어 인터셉터가 false를 반환하고, 요청이 들어온 주소를 로그에 찍은 뒤 컨트롤러로 요청을 반려한 것을 확인할 수 있다(=로그에 hello가 찍히지 않았다).