2023.05.16 - [Java & Spring] - Filter
지난번에 정리한 Filter에 이어 Interceptor를 정리하였다.
Interceptor
Interceptor 인터셉터
Interceptor는 위의 이미지 처럼, Spring Context 영역에서 실행되는, Spring이 제공하는 기술이다. 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있는 기능을 제공한다.
Filter 이후에, Spring Context 영역에서 실행되는 Dispatcher Servlet은 요청에 따른 컨트롤러를 매칭시키위해, 핸들러 매핑을 수행하고 HandlerExecutionChain
을 리턴한다.
FilterChain이 각 필터를 순서에 따라 다 거친 뒤에 서블릿을 호출한 것 처럼, HandlerExecutionChain
은 각 인터셉터를 순서에 따라 다 거친 뒤에 컨트롤러를 호출한다.
Dispatcher Servlet의 동작 방식
- 필터 등을 거친 뒤, Dispatcher Servlet이 클라이언트 요청을 받는다.
- Dispatcher Servlet은
doService
메소드를 수행하면서, 필요한 속성값들을 세팅한다. - 이후,
doDispatch
메소드를 수행해 아래 작업을 처리한다.
- Dispatcher Servlet은
- Dispatcher Servlet은 요청을 보고 이 요청을 위임할 컨트롤러를 찾는다. : Handler Mapping
- 찾은 컨트롤러로 요청을 위임하기 위해서, 이 컨트롤러에 대한 핸들러 어댑터를 찾아서, 어댑터를 이용해 요청을 전달한다. : Handler Adapter
- 핸들러 어댑터가 요청을 컨트롤러로 위임한다.
- 비지니스 로직을 수행한다.
- 컨트롤러가 Response를 반환한다.
- 핸들러 어댑터가 전달받은 Response를 처리하여, Dispatcher Servlet으로 전달한다.
- 클라이언트로 전달받은 Response를 반환하여 응답을 마무리한다.
핸들러와 컨트롤러
스프링 MVC는 웹 요청을 처리할 수 있는 범용적인 프레임워크를 제공하고있다.
이에 따라, 스프링 MVC는 웹 요청을 실제로 처리하는 객체를 핸들러(Handler)라고 표현하고 있다.
@Controller 적용 객체나 Controller 인터페이스를 구현한 객체 모두 스프링 MVC입장에서는 핸들러이다.
Dispatcher Servlet이 핸들러(컨트롤러)를 찾는 방법
Dispatcher Servlet은 요청을 확인해서 적절한 컨트롤러를 찾는데,
이 때, “적절한 컨트롤러를 찾는 것”은 RequestMappingHandlerMapping
이 찾아준다.
이 RequestMappingHandlerMapping
은 HandlerMapping
의 구현체인데,HandlerMapping
은 HTTP 요청과 핸들러를 매핑 해주는 역할을 하는 인터페이스이다.
HandlerMapping
의 getHandler
메소드를 통해서 주어진 HTTP 요청에 매핑된 핸들러를 반환한다. 정확히는 매핑된 핸들러를 포함한 HandlerExecutionChain
을 반환한다.
RequestMappingHandlerMapping
이 상속하고 있는 클래스들을 통해서 getHandler
메소드가 구현된 내부를 살펴보면, Map에 path(경로, T
)를 key로, 매핑된 핸들러를 value로 등록하여 관리한다.
MappingRegistry
가 관리하는 MappingRegistration
을 보면, 매핑될 경로 mapping
와 HandlerMethod
를 관리하고 있다.
HandlerMethod
는 매핑할 경로에 해당하는 컨트롤러 메소드를 가진다.
컨트롤러 메소드를 그냥 사용하지 않고, HandlerMethod
로 감싸서 관리하는 이유는 HandlerMethod
가 메소드와 함께 메소드에 대한 부가적인 정보를 가지고 있기 때문이다.
이렇게 코드를 열어서 확인하는 이유는,HandlerMapping
을 거치면, 컨트롤러를 바로 반환하는 것이 아니라,
주어진 요청 경로에 매핑된 컨트롤러 메소드를 가지고 있는 HandlerMethod
를 포함한,HandlerExecutionChain
를 반환한다는 것이다.
정리하면,
- Dispatcher Servlet이 HandlerMapping 인터페이스의 getHandler 메소드를 호출한다.
- HandlerMapping의 구현체
RequestMappingHandlerMapping
가 주어진 경로에 매핑된 핸들러(인터셉터, 컨트롤러)를 가진HandlerExecutionChain
을 반환한다.
요청을 핸들러로 바로 위임하지 않고, 핸들러 어댑터를 통해 위임, 전달하는 이유
어차피 요청은 컨트롤러에서 비즈니스 로직을 수행하며 처리할 것이고, Dispatcher Servlet이 컨트롤러를 찾았는데 왜 바로 컨트롤러로 넘기지 않고, 핸들러 어댑터를 통해 요청을 위임하는 걸까?
위에서 설명한 과정을 통해 Handler Mapping으로 HandlerExecutionChain을 받았으면, Chain에서 Interceptor를 꺼내 잘 실행하고, Handler Method를 잘 꺼내서 컨트롤러 메소드에 위임하면 되지 않을까?
맞다. 전달받은 HandlerExecutionChain을 통해 컨트롤러(핸들러)를 실행하면 된다. 하지만, 핸들러의 구현 방식이 다양할 수 있기 때문이다.
DispatcherServlet은 핸들러 객체의 실제 타입에 상관없이, 실행 결과를 ModelAndView라는 타입으로만 받을 수 있으면 된다. 그런데 핸들러의 실제 구현 타입에 따라 ModelAndView를 리턴하는 객체도있고, 그렇지 않은 구현 객체도 있다. HandlerAdapter가 바로 이변환을 수행해준다.
즉, 스프링의 역사를 지나오면서 핸들러를 다양한 방식으로 구현하였고, 이에 대응하기 위해 HandlerAdapter
인터페이스를 통해 어댑터 패턴을 적용하여 핸들러의 구현방식에 제약없이 요청을 위임할 수 있다. 서블릿과 핸들러 간에 느슨한 결합이 이루어진 것이다.
응답 전달시, 핸들러 어댑터가 하는 역할
그렇다면, 비즈니스 로직을 처리한 뒤 컨트롤러가 전달하는 응답은 왜 핸들러 어댑터를 거쳐야할까?
컨트롤러가 ResponseEntity를 반환하면, 핸들러 어댑터를 거치면서 전달받은 응답을 적절한 형태로 변환한다.
HttpEntityMethodProcessor가 MessageConverter를 사용해 응답 객체를 직렬화하고 응답 상태(HttpStatus)를 설정한다.
인터셉터의 실행시점
아래 코드는 Dispatcher Servlet의 doDispatch
메소드, processDispatchResult
메소드의 일부이다.
doDispatch 메소드 코드 일부를 보면,getHandler
로 HandlerExecutionChain
을 찾고,getHandlerAdapter
로 HandlerAdapter
을 찾고,HandlerExecutionChain.applyPreHandle
을 통해 핸들러 실행 전에 인터셉터를 실행하고,HandlerAdapter.handle
로 핸들러 어댑터를 통해 핸들러를 invoke하여 비즈니스 로직을 수행하고,HandlerExecutionChain.applyPostHandle
을 통해 핸들러 실행 후에 인터셉터를 실행하고,processDispatchResult
메소드를 수행한다.
processDispatchResult 메소드 코드 일부를 보면,HandlerExecutionChain.triggerAfterCompletion
로 요청 처리가 다 끝나고 마지막으로 인터셉터를 수행한다.
이렇게, 인터셉터는 총 3번의 시점에서 실행된다.
따라서, 인터셉터 인터페이스는 각 시점에서 실행하는 메소드 3개를 가지고 있다.
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 {
}
}
서블릿 필터는 init, doFilter, destory 메소드를 가지고 있었다.
이중, doFilter 메소드 하나로 FilterChain을 실행했는데, 인터셉터는 컨트롤러 호출 전( preHandle ), 호출 후( postHandle ), 요청 완료 이후( afterCompletion )와 같이 단계적으로 잘 세분화 되어 있다.
preHandle 메소드 - 핸들러(컨트롤러)가 invoke되기 전에 인터셉터가 실행된다.
핸들러 어댑터가 핸들러를 invoke하기 전에, 인터셉터를 호출한다.
preHandle 메소드가 true를 리턴하면 DispatcherServlet와 HandlerExecutionChain은 그 다음 로직을 수행한다. 하지만, false를 리턴한다면, 핸들러 어댑터가 실행되지도, HandlerInterceptor의 postHandle이 실행되지도 않는다. ( 예외 처리를 위해 afterCompletion은 실행된다. )
preHandle 시에 처리되는 인터셉터는 컨트롤러 이전에 처리해야 하는 전처리 작업을 수행하거나, 요청 정보를 가공하거나 추가하는 경우에 적합하다.
postHandle 메소드 - 핸들러가 정상적으로 수행된 후에 인터셉터가 실행된다.
핸들러 어댑터가 핸들러를 invoke하여 비즈니스 로직을 수행한 후에, 인터셉터를 호출한다.
preHandle 메소드에서 혹은 핸들러가 처리되면서 예외가 발생하면 postHandle의 인터셉터는 실행되지 않는다.
핸들러 처리 이후, 필요한 후 작업이 필요한 경우에 적합하다.
afterCompletion 메소드 - 모든 처리를 마친 후 마지막 인터셉터가 실행된다.
모든 작업, 처리를 마친 후에(뷰가 렌더링된 후에), 인터셉터를 호출한다.
예외가 발생해도, afterCompletion은 반드시 호출된다.
요청 처리 중에 사용한 리소스를 반환할 때 적합하다.
또, 세번째 파라미터 ex가 예외를 받으므로, 어떤 예외가 발생했는지 로그로 출력할 때에도 적합하다. postHandle도 핸들러 처리 후에 호출되므로 예외 처리를 하기 좋다고 생각할 수 있지만, postHandle은 핸들러 처리 중 예외가 발생하면 호출되지 않으므로, 예외와 상관없이 반드시 호출되는 afterCompletion에서 예외와 무관한 공통적인 예외 처리를 하는 것이 좋다.
Interceptor의 활용 예시
- 세부적인 보안 및 인증 / 인가 공통 작업
- 스프링의 지원을 받으므로 보다 세부적인 작업이 가능하다
- 공통적인 보안 작업은 Filter에서 처리하는 것이 좋다.
- API 호출에 대한 로깅, 감사
- 핸들러(컨트롤러)로 넘겨주는 데이터 가공
Interceptor vs AOP
AOP를 복기 시켜보면, 동일하게 어떠한 부가 로직을 수행하기 위함이므로, 인터셉터나 필터나 AOP나 공통적인 목적은 동일하다.
순서
컴파일시에 위빙하는 등 AOP를 구현하는 방법은 3가지가 있지만, 그 중 프록시 위빙을 생각해보면, 특정 메소드에 대한 프록시를 만들어두고, 그 메소드가 호출될때 실제 메소드가 수행되는 구조였다.
그렇기 때문에 순서는 다음과 같을 것이다.
- Filter
- Interceptor(PreHandle)
- AOP
- Interceptor(PostHandle, AfterCompletion)
- Filter
컨트롤러의 부가 로직을 수행하기에 AOP가 좋을까, Interceptor가 좋을까?
컨트롤러는 각 컨트롤러가 내부 파라미터로 구분되기보다는, 매핑되는 경로로 구분된다.
Interceptor는 이에 걸맞게 주소로 핸들러를 매핑하기 때문에 특정 컨트롤러 이전에 부가로직을 수행하기 적합하다.
하지만, AOP는 “메소드”를 대상으로한다. 다양한 포인트 컷들을 사용해서 메소드의 실행 전 후에 따라 부가로직을 적용한다.
또, AOP는 JoinPoint로부터 파라미터를 불러와야한다.
AOP도 메소드의 파라미터를 불러올 수는 있다. 아래 코드는 AOP 1편에서 소개했던, 예시 코드이다.
package org.springframework.samples.petclinic.owner;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
@Component
@Aspect
public class LogAspect {
Logger logger = LoggerFactory.getLogger(LogAspect.class);
@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] parameters = joinPoint.getArgs();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// @LogExecutionTime이 붙어있는 타겟 메소드를 실행
Object proceed = joinPoint.proceed();
stopWatch.stop();
logger.info(stopWatch.prettyPrint());
return proceed;
}
}
위 코드에서는 아래와 같이 파라미터를 불러올 수 있다.
Object[] parameters = joinPoint.getArgs();
만약 이 AOP Aspect가 특정 컨트롤러에 수행되고,
request, response 파라미터가 필요하다면(컨트롤러의 부가 로직이니 아마도 필요할 일이 많을 것이다.),
불필요한 로직이 추가되는 것이다.
인터셉터는 아래와 같이 인터셉터의 파라미터로 request, response가 주어진다.
@Component // bean 등록
@RequiredArgsConstructor
public class HasAnimalsInterceptor implements HandlerInterceptor {
private final AnimalService animalService; // DI
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String email = authentication.getName();
List<Animal> animalList = animalService.findByMemberEmail(email);
if(animalList.size() == 0){
response.sendRedirect("/error/not-animals");
return false;
}
return true;
}
}
따라서 컨트롤러의 부가 로직을 수행하기에는 Interceptor가 더 적합하다.
Filter vs Interceptor
소속된 컨테이너 영역
Filter는 서블릿 컨테이너(톰캣과 같은 웹 컨테이너)의 영역에서 실행된다.
그렇기 때문에 Filter에서 발생하는 예외는 Spring의 예외처리가 불가능하다. 예외가 발생했을 경우, HttpServletResponse에 응답 코드를 설정하고, 에러 응답을 설정하는 등 직접 처리가 필요하다.
반면에, Interceptor는 스프링 컨테이너의 영역에서 실행된다.
따라서 스프링의 지원이 필요할 경우에는 Interceptor를 사용하는 것이 좋다.
단, Filter도 스프링 빈으로 등록할 수 있고, 빈을 주입받을 수 있다.
Request, Response 객체 교체 불가
Filter는 FilterChain에 속하여있는 그 다음의 Filter를 호출할 때, 아래와 같이 호출한다.
public MyFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// 개발자가 다른 request와 response를 넣어줄 수 있음
chain.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse());
}
}
그렇기 때문에 ServletRequest, ServletReponse 파라미터 객체를 다른 객체로 교체할 수 있다.
Interceptor는 HandlerExecutionChain에 속하여있는 그 다음의 인터셉터를 직접 호출하지 않는다.
아래는 HandlerExecutionChain이 각 인터셉터의 preHandle메소드로 호출하여 실행하는 applyPreHandle
메소드 코드이다.
호출할 Interceptor들을 for문을 통해, HandlerExecutionChain이 직접 호출한다.
각 Interceptor는 리턴값으로 그 다음 인터셉터를 호출하지 않고, boolean 여부를 리턴한다.
public class MyInterceptor implements HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// Request/Response를 교체할 수 없고 boolean 값만 반환할 수 있다.
return true;
}
}
전달받는 파라미터
Filter 구현체 코드를 보면, Request, Response 그리고 그 다음 Filter를 호출하기 위한 FilterChain이 파라미터로 주어진다.
public MyFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// 개발자가 다른 request와 response를 넣어줄 수 있음
chain.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse());
}
}
반면에, Interceptor 구현체 코드를 보면, Request, Response 그리고 핸들러가 파라미터로 주어진다.
주어진 핸들러를 통해 어떤 modelAndView가 반환되었는지의 응답정보 등을 알 수 있다.
public class MyInterceptor implements HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// Request/Response를 교체할 수 없고 boolean 값만 반환할 수 있다.
return true;
}
}
따라서, 핸들러의 정보가 필요한 경우 Interceptor가 적합할 것이다.
전체 구조
Reference
https://velog.io/@timssuh/Spring-Boot에서-커스텀-Interceptor-사용해보기
Spring Boot에서 커스텀 Interceptor 사용해보기
상황 온라인 동물병원 프로젝트 예약서는 동물을 갖고 있는 고객만이 들어갈 수 있도록 한다. 매번 검사할 수 있지만, 너무 중복이 많다. 공통처리. 그중 Interceptor를 사용해보자!. 사전 지식 Interc
velog.io
https://mangkyu.tistory.com/173
[Spring] 필터(Filter) vs 인터셉터(Interceptor) 차이 및 용도 - (1)
Spring은 공통적으로 여러 작업을 처리함으로써 중복된 코드를 제거할 수 있도록 많은 기능들을 지원하고 있다. 이번에는 그 중에서 필터(Filter) vs 인터셉터(Interceptor)의 차이에 대해 알아보고자
mangkyu.tistory.com
https://tecoble.techcourse.co.kr/post/2021-07-15-dispatcherservlet-part-2/
DispatcherServlet - Part 2
지난 1편에서는 DispatcherServlet 정의, 설정 방법, 동작 흐름에 대해 알아봤다. 이번 2편에서는 DispatcherServlet의 동작 원리를 코드와 함께 살펴보자.
tecoble.techcourse.co.kr
HandlerExecutionChain
(package private) void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, java.lang.Exception ex) Trigger afterCompletion callbacks on the mapped HandlerInterceptors.
docs.spring.io
MappedInterceptor (Spring Framework 6.0.9 API)
postHandle Interception point after successful execution of a handler. Called after HandlerAdapter actually invoked the handler, but before the DispatcherServlet renders the view. Can expose additional model objects to the view via the given ModelAndView.
docs.spring.io
https://github.com/binghe819/TIL/blob/master/Spring/MVC/Spring%20MVC%20flow.md
GitHub - binghe819/TIL: 📚 Today I Learned. 기록하자.
📚 Today I Learned. 기록하자. Contribute to binghe819/TIL development by creating an account on GitHub.
github.com
'Java & Spring' 카테고리의 다른 글
Consistent Hashing (0) | 2023.06.16 |
---|---|
Concurrent Hash Map과 HashMap, HashTable, Synchronized Hash Map (1) | 2023.05.30 |
Filter (0) | 2023.05.16 |
이펙티브 자바 - 아이템 21: 인터페이스는 구현하는 쪽을 생각해 설계하라 (0) | 2023.05.08 |
Spring AOP - Proxy, Dynamic Proxy (0) | 2023.05.03 |