Filter & Interceptor

강서진·2024년 2월 13일

Filter & Interceptor

기본적으로 요청이 들어오면 필터를 가장 먼저 거쳐가게 된다. 필터를 거친 후 디스패처 서블릿을 지나고, 핸들러 인터셉터를 지나고, 컨트롤러로 내용이 들어온다. 필터는 톰캣이 관리하는 웹 컨텍스트에서 작동하며, 스프링의 관리를 받지 않는다. 반면 인터셉터는 스프링에서 관리한다.

최전방에 있기 때문에 필터는 클라이언트에서 전달된 가장 날 것의 리퀘스트를 받는다. 때문에 필터는 들어온 데이터를 다른 데이터로 변환시켜 안쪽으로 보낸다든지, 들어온 데이터에 대해 내용을 기록하든지 로그 시스템을 많이 활용한다.

인터셉터는 스프링에서 관리하는 빈으로, 컨트롤러로 보내기 바로 전 단계를 맡고 있다. 이미 핸들러 맵핑을 통해 어느 컨트롤러로 보낼지 요청의 방향이 정해져있기 때문에, 해당 컨트롤러가 가진 애너테이션을 찾아 작업을 한다든지, 헤더의 내용을 참고하여 권한을 검사하든지 등 주로 인증 작업을 맡는다. 정상적인 데이터가 전달되었는지, 해당 주소로 보내는 이 컨트롤러에 어떤 권한을 가지고 있는지 확인하여 승인하거나 반려하는 등의 역할을 하는 것이다.

Filter

다음과 같은 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를 복사해주어야 응답이 제대로 반환된다.

이렇게 필터는 들어오고 나가는 원본 데이터를 확인하는 로깅으로 활용할 수 있다.

Interceptor

동일한 프로젝트에 인터셉터를 추가해본다. 인터셉터는 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가 찍히지 않았다).

0개의 댓글