0+ 스프링/0+ 스프링 MVC

[스프링 MVC] 서블릿 필터가 있는데 인터셉터는 왜 나온거지?

힘들면힘을내는쿼카 2023. 5. 26. 15:09
728x90
반응형

[스프링 MVC] 서블릿 필터가 있는데 인터셉터는 왜 나온거지?

 

먼저 보는 결론

필터와 스프링 인터셉터는 웹과 관련된 👀 공통 관심사를 해결하기 위한 기술입니다.

 

 

인터셉터와 필터는 관리되는 영역이 다릅니다.
필터는 서블릿 컨테이너에서 관리되지만, 인터셉터는 스프링 컨테이너에서 관리됩니다.

따라서 인터셉터는 스프링이 처리해주는 내용을 적용받을 수 있지만, 필터는 적용받을 수 없습니다.
가장 대표적으로 필터 기술은 스프링에 의한 예외처리가 되지 않는다는 것 입니다.

 

그런데 현재는 필터를 스프링 빈으로 등록할 수 있습니다.
그 이유는 여기를 참고하시면 됩니다.^^ 서블릿 필터를 어떻게 스프링 빈으로 관리할 수 있을까?

 

[스프링 MVC] 서블릿 필터를 어떻게 스프링 빈으로 관리할 수 있을까?

[스프링 MVC] 서블릿 필터를 어떻게 스프링 빈으로 관리할 수 있을까? 서블릿 필터는 서블릿 컨테이너 영역입니다. 그런데 우리는 서블릿 필터 구현체를 작성하고 빈으로 등록할 수 있습니다. 필

howisitgo1ng.tistory.com

 

필터?

웹 세상에서 필터란 J2EE 표준 스펙으로 디스패처 서블릿에 요청이 전달되기 전/후
모든 url 패턴에 맞는 모든 요청에 대해 부가적인 기능을 추가할 수 있는 방법을 제공합니다.

 

참고로 필터에는 스프링 인터셉터에는 제공하지 않는 강력크한 기능이 있는데,
바로바로바로
chain.doFilter(request, response);를 호출해서 다음 필터 또는 서블릿을 호출할 때 request, response를 다른 객체로 바꿔치기할 수 있습니다.

 

ServletRequest , ServletResponse 를 구현한 다른 객체를 만들어서 넘기면 해당 객체가 다음 필터 또는 서블릿에서 사용 됩니다.

 

디스패처 서블릿?

디스패처 서블릿은 스프링의 가장 앞단에 존재하는 컨트롤러 입니다.
즉, 스프링으로 들어오는 문을 지키는 수문장이 바로 디스패처 서블릿(프론트 컨트롤러) 입니다.

 

필터와 스프링의 구조

 

필터는 스프링 컨테이너에서 관리되지 않는 것을 확인할 수 있습니다.

 

다만, 현재 DelegatingProxyFilter의 등장으로 필터도 스프링 빈으로 등록할 수 있습니다.
다시말하면 필터 자체는 스프링에서 관리할 수 없지만,
필터 인터페이스를 구현한 커스텀 필터 사용하여 스프링 빈으로 관리할 수 있습니다.

 

참고

 

[스프링 MVC] 서블릿 필터를 어떻게 스프링 빈으로 관리할 수 있을까?

[스프링 MVC] 서블릿 필터를 어떻게 스프링 빈으로 관리할 수 있을까? 서블릿 필터는 서블릿 컨테이너 영역입니다. 그런데 우리는 서블릿 필터 구현체를 작성하고 빈으로 등록할 수 있습니다. 필

howisitgo1ng.tistory.com

 

필터 인터페이스

스프링 환경에서 필터를 추가하기 위해서는 필터 인터페이스(javax.servlet)를 구현해야 합니다.

 

필터 인터페이스는 다음과 같이 3가지 메소드가 존재합니다.

  • init()
  • doFilter()
  • destroy()

 

필터 인터페이스

public interface Filter {

    public default void init(FilterConfig filterConfig) throws ServletException {}

    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException;

    public default void destroy() {}
}

 

init()

init() 메소드는 필터 객체를 초기화하고, 서비스에 추가하기 위한 메소드 입니다.
서블릿 컨테이너가 1회 init()를 호출하여 필터 인스턴스를 초기화하면 이후의 요청들은 doFilter를 통해 처리됩니다.

 

doFilter()

doFilter() 메소드는 url-pattern에 맞는 모든 HTTP 요청이 디스패처 서블릿으로 전달되기 전에 서블릿 컨테이너에 의하여 실행되는 메소드 입니다.

 

여기서 FilterChain 파라미터로 인하여 다음 대상으로 요청을 전달합니다.
chain.doFilter() 전/후에 우리가 커스텀한 처리 기능을 넣어줌으로써 원하는 결과를 얻을 수 있습니다.

// 요청을 실행하는 메소드, 필터가 여러개면 다음 필터를 호출한다.
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    LocalDateTime now1 = LocalDateTime.now();
    System.out.println("["+now1+"] 안녕하세요.");
      // 전
    chain.doFilter(request, response);
      // 후
    LocalDateTime now2 = LocalDateTime.now();
    System.out.println("["+now2+"] 잘가요~");
}

 

destroy()

destroy() 메소드는 필터 객체를 서비스에서 제거하고 사용하는 자원을 반환하기 위한 메소드 입니다.
서블릿 컨테이너에서 1회 호출되며 이후에는 doFilter()에 의해 처리되지 않습니다.

 

필터 예제

스프링 부트 환경에서 필터를 구현해보자.

@Component
public class CustomFilter implements Filter {

    // 필터 초기화 메소드, 서블릿 컨테이너가 생성될 때 호출
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        LocalDateTime now1 = LocalDateTime.now();
        System.out.println("["+now1+"] 셋, 둘, 하나!");

        Filter.super.init(filterConfig);

        LocalDateTime now2 = LocalDateTime.now();
        System.out.println("["+now2+"] 시작한다!");
    }

    // 요청을 실행하는 메소드, 필터가 여러개면 다음 필터를 호출한다.
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        LocalDateTime now1 = LocalDateTime.now();
        System.out.println("["+now1+"] 안녕하세요.");

        chain.doFilter(request, response);

        LocalDateTime now2 = LocalDateTime.now();
        System.out.println("["+now2+"] 잘가요~");
    }

    // 필터 종료 메소드, 서블릿 컨테이너가 종료될 때 호출
    @Override
    public void destroy() {
        LocalDateTime now1 = LocalDateTime.now();
        System.out.println("["+now1+"] 고생했다!");

        Filter.super.destroy();

        LocalDateTime now2 = LocalDateTime.now();
        System.out.println("["+now2+"] 푹쉬어~");
    }
}

 

필터 과정을 보기위해 간단한 컨트롤러를 작성하자.

@RestController
public class TestController {

    @GetMapping("/")
    public String test() {
        LocalDateTime now = LocalDateTime.now();
        return now.toString();
    }
}

 

결과
애플리케이션을 실행하고 “/”호출하고 종료하면 다음과 같은 결과를 얻을 수 있습니다.

[2023-05-25T18:59:18.666116] 셋, 둘, 하나!
[2023-05-25T18:59:18.666365] 시작한다!
//...//
[2023-05-25T19:00:10.035357] 안녕하세요.
[2023-05-25T19:00:10.083372] 잘가요~
[2023-05-25T19:00:48.514398] 고생했다!
[2023-05-25T19:00:48.514487] 푹쉬어~

 

아래와 같은 흐름으로 진행된 것을 알 수 있습니다.^^

init() -> doFilter() -> doFilter() -> destroy()

 

필터 🐶댕꿀인데 왜 인터셉터가 나온거지? 🤔

인터셉터는 J2EE 표준 스펙인 필터와 달리 스프링이 제공하는 기술입니다.
그말은 스프링 컨테이너에 직접 관리되며,
디스패처 서블릿이 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있는 기능을 제공합니다!!

 

이말은 필터와 같이 인터셉터도 웹과 관련된 공통 관심사항을 효과적으로 해결할 수 있는 기술입니다.
(서블릿 필터가 서블릿이 제공하는 기술이면, 인터셉터는 스프링 MVC가 제공하는 기술)

 

인터셉터 구조

디스패처 서블릿은 핸들러 매핑을 통해 적절한 컨트롤러를 찾습니다.
그 결과로 실행 체인(HandlerExecutionChain)을 돌려줍니다.


실행 체인(HandlerExecutionChain)은 1개 이상의 인터셉터가 등록되어 있으면
순차적으로 인터셉터들을 거쳐 컨트롤러가 실행되도록 하고,
인터셉터가 없다면 컨트롤러를 실행합니다.

 

인터셉터 흐름

HTTP 요청 -> WAS -> 필터 -> 디스패처 서블릿 -> 스프링 인터셉터 -> 컨트롤러

1. 필터를 거쳐 요청이 디스패처 서블릿으로 들어온다.
2. 디스패처 서블릿은 핸들러 매핑을 통해 적절한 컨트롤러를 찾는다.
3. 그 결과 HandlerExcutionChain을 돌려준다.
4. HandlerExcutionChain은 인터셉터를 찾는다.
5. 1개 이상의 인터셉터가 등록되어 있으면 인터셉터를 거쳐 컨트롤러를 실행한다.
6. 등록된 인터셉터가 없으면 컨트롤러를 실행한다.

 

다시 말하면, 인터셉터는 스프링 컨테이너 안에서 동작하기 때문에,
필터를 거치고 디스패처 서블릿이 요청을 받은 이후 동작합니다.

 

스프링 MVC 구조에 특화된 필터 기능을 제공하기 위해서 나온 기술입니다.

 

참고로 필터와 마찬가지로 인터셉터는 체인으로 구성되어 중간에 자유롭게 인터셉터를 추가할 수 있습니다.

 

인터셉터 인터페이스

스프링 인터셉터를 사용하기 위해서는 인터셉터 인터페이(HandlerInterceptor)를 구현해야 합니다.

 

인터셉터 인터페이스는 다음과 같이 3가지 메소드가 존재합니다.

  • preHandle()
  • postHandle()
  • afterCompletion()

 

인터셉터 인터페이스

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()

컨트롤러가 호출되기 전에 실행됩니다.(정확히는 핸들러 어댑터 호출 전에 실행)
그렇기 때문에 컨트롤러 호출전에 처리해야 하는 작업이나 요청 정보를 가공할 수 있습니다.

3번째 파라미터인 handler는 핸들러 매핑이 찾아준 컨트롤러 빈에 매핑되는 HandlerMethod라는 새로운 타입의 객체입니다.(@RequestMapping이 붙은 메소드의 정보를 추상화한 객체)

여기서 preHandle()의 반환 타입은 boolean인것을 확인할 수 있는데,
반환값이 true이면 다음으로 진행하고
반환값이 false이면 더 이상 진행하지 않습니다.(다음 인터셉터 또는 컨트롤러는 진행되지 않음)

 

postHandle()

컨트롤러 호출 이후에 실행 됩니다.(정확히는 핸들러 어댑터 호출 후에 실행)
그렇기 때문에 컨트롤러 실행 이후에 처리해야하는 작업이나 응답 정보를 가공할 수 있습니다.

3번째 파라미터로 ModelAndView 타입이 제공되는 것을 확인할 수 있습니다.
(HTTP API 기반의 애플리케이션을 자주 사용면서 자주 사용되지 않습니다.)

다른 하위 계층에서 작업을 진행하다가 중간에 예외가 발생하면 postHandle은 호출되지 않습니다.

 

afterCompletion()

모든 뷰(View)에서 최종 결과를 생성하는 작업을 포함하여 모든 작업이 완료된 후에 실행되는 메소드 입니다.
요청 처리 중에 사용한 리소스를 반활할 때 사용하기 적합합니다.

afterCompletion()은 하위 계층에서 예외가 발생하더라도 반드시 호출됩니다.
따라서 4번째 파라미터인 Exception을 이용하여 어떤 예외가 발생했는지 로그로 출력할 수 있습니다.

컨트롤러 하위 계층에서 예외가 발생하면 postHandle()은 호출되지 않습니다.
따라서 예외와 무관하게 공통 처리하기 위해서는 afterCompletion()을 사용해야 합니다.^^

 

인터셉터 말고 AOP를 사용할 수 있지 않나요? 🤔

인터셉터 대신에 AOP를 사용할 수 있습니다!
하지만, 과 관련된 공통 관심사항을 효과적으로 해결하는데 AOP는 적합하지 않습니다.

  • 컨트롤러는 타입과 실행 메소드가 제각각
    • 포인트컷 작성이 어려움(적용할 메소드 선별 작업이 어려움)
  • 컨트롤러는 파라미터나 반환값이 제각각
  • AOP에서HttpServletRequest/Response를 객체를 얻기 어려움

 

스프링 MVC를 사용하고, 필터를 반드시 사용해야 하는 상황이 아니라면 인터셉터를 사용하는 것이 편리합니다.^^👍

 

예외 처리

스프링의 예외 처리

일반적으로 스프링을 사용하면 ControllerAdviceExceptionHandler를 이용하여 예외처리를 기능을 사용합니다.
이를 통해 예외가 서블릿으로 전달되지 않고 처리됩니다.

@RestControllerAdvice
public class BasicExceptionHandler {
    @ExceptionHandler(BasicException.class)
    public ResponseEntity<Object> basicExceptionHandle(BasicException e) {
        return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
    }
}

 

필터에서 예외 처리

필터는 디스패처 서블릿의 앞 영역인 서블릿 컨테이너에서 관리되기 때문에 스프링의 지원을 받을 수 없습니다.
그래서 필터에서 BasicException이 던져졌다면, 에러가 처리되지 않고 서블릿까지 전달 됩니다. 😇

 

서블릿은 예외가 내부에서 핸들링 되기를 기대했지만, 예외가 그대로 올라와서 BasicException을 만난 상황 입니다.
따라서 내부에 문제가 있다고 판단하여 500 status를 반환 합니다. 😇😇😇😇😇
(???: 개발자 이놈아 왜 서블릿까지 예외를 보낸거야!!)

 

따라서 아래와 같이 응답 객체(ServletResponse)에 예외 처리가 필요합니다.

@Component
public class FilterExceptionHandleFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            System.out.println("FilterExceptionHandleFilter 호출");
            chain.doFilter(request, response);
            System.out.println("FilterExceptionHandleFilter 종료");
        }
          // FilterExceptionHandleFilter 의 하위 필터에서 발생한 예외를 처리한다. 
          catch (RuntimeException e) {
            System.out.println(e.getMessage());
            HttpServletResponse servletResponse = (HttpServletResponse) response;
            servletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            servletResponse.getWriter().println(e.getMessage());
        }
    }
}

 

구현에 대한 예제는 해당 포스팅을 확인해주세요^^

2023.05.26 - [0+ 스프링/0+ 스프링 MVC] - [스프링 MVC] 서블릿 필터 예외처리 방법

 

[스프링 MVC] 서블릿 필터 예외처리 방법

[스프링 MVC] 서블릿 필터 예외처리 방법 RestControllerAdvice로 서블릿 필터 예외처리가 안되는 이유 일반적으로 스프링을 사용하면 ControllerAdvice와 ExceptionHandler를 이용하여 예외처리를 기능을 사용

howisitgo1ng.tistory.com

 

Request/Response 조작? 😈

필터는 Request/Response를 조작할 수 있지만, 인터셉터는 조작할 수 없습니다.

 

조작?
여기서 조작한다는 것은 내부 상태를 변경한다는 것이 아니라 다른 객체로 바꿔치기 한다는 의미 입니다.

 

우리는 🧑‍💻 개발자 이니 인터셉터와 필터의 구현 코드를 보면서 이야기 해봅시다!

 

CustomFilter

public class CustomFilter implements Filter {

    // 요청을 실행하는 메소드, 필터가 여러개면 다음 필터를 호출한다.
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 개발자가 다른 request와 response를 넣어줄 수 있다.
        chain.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse());
    }
}

 

필터가 다음 필터를 호출하기 위해서는 필터 체이닝(다음 필터 호출)을 해주어야 합니다.
그리고 이때 chain.doFilter()Request/Response 객체를 넘겨주므로
우리가 원하는 Request/Response 객체를 넣어줄 수 있습니다.
(NPE가 나겠지만 null도 넣어줄 수 있습니다.)

 

CustomInterceptor

public class CustomInterceptor implements HandlerInterceptor {

    // 컨트롤러가 호출되기 전에 실행
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
          // Request/Response를 교체할 수 없고 boolean 값만 반환할 수 있다.
        return false;
    }
}

 

디스패처 서블릿이 여러 인터셉터 목록을 가지고 있고, for 문으로 순차적으로 실행시킵니다.
그리고 true를 반환하면 다음 인터셉터가 실행되거나 컨트롤러로 요청이 전달되며, flase가 반환되면 요청이 중단됩니다.
Request/Response를 전달할 수 없다는 것을 알 수 있습니다.

 

스프링 시큐리티

대표적으로 필터를 사용하여 인증, 인가에 사용하는 기술로 스프링 시큐리티가 있습니다.
서블릿 필터를 사용하기 때문에 스프링 시큐리티의 특징 중 하나는 스프링 MVC에 종속적이지 않다는 것 입니다.

 

정리

 

필터와 스프링 인터셉터는 웹과 관련된 공통 관심사를 해결하기 위한 기술입니다.

  • 필터
    • 공통된 보안 및 인증/인가
      • 전역적으로 해야하는 보안 검사(XSS 방어 등)를 하여 올바른 요청이 아닐 경우 차단
    • 모든 요청에 대한 로깅
    • 이미지/데이터 압축 및 문자열 인코딩
    • 스프링과 분리되어야하는 기능
  • 인터셉터
    • 세부적인 보안 및 인증/인가
      • 특정 그룹의 사용자는 어떤 기능을 사용하지 못하게 처리하는 경우
    • API 호출에 대한 로깅
    • 컨트롤러로 넘겨주는 데이터 가공

 

 

참고

 

 

 

 

728x90
반응형