인터셉터

박진선·2024년 10월 8일
0
post-custom-banner

인터셉터란?

서블릿 필터와 같이 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기술이다, 서블릿 필터가 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링 MVC가 제공하는 기술이다.
둘다 웹과 관련된 공통 관심 사항을 처리하지만, 적용되는 순서와 범위, 그리고 사용방법이 다르다.

인터셉터 흐름도

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 ->... -> 컨트롤러

스프링 MVC가 제공하는 기능이기 때문에 결국 디스패처 서블릿 이후에 등장, 스프링 MVC의 시작점이 디스패처 서블릿이라고 생각하면 된다.

인터셉터 메소드 실행원리

DispatcherServlet에서 HandlerExecutionChain을 호출하여 인터셉터의 각 메소드가 동작하는데 코드에 보이는것과 같이 HandlerInterceptor 구현체의 리스트를 필드로 가지고 있다.

preHandle

HandlerExecutionChain의 applyPreHandle 메소드가 interceptor 리스트의 preHandle 메소드를 호출하는데 각 인터셉터가 true 반환하면 다음 인터셉터가 진행되고 특정 인터셉터가 false를 반환할 경우 return; 실행되어 나머지 인터셉터는 물론, 핸들러 어댑터도 호출되지 않는다. 그냥 DispatcherServlet이 종료된다고 보면 된다.

postHandle

핸들러 어뎁터가 컨트롤러 호출 후에 서블릿에 ModelAndView를 반환 하고 나서 호출된다.
HandlerExecutionChain의 applyPostHandle 메소드가 interceptor 리스트의 postHandle 메소드를 호출한다. ModelAndView 를 파라미터로 받기 때문에 무언가 추가 작업을 할 수 있다.

afterCompletion

postHandle이 호출된 후 약간의 추가 로직 뒤에 호출된다.

특정 인터셉터의 preHandle 메소드가 false를 반환 하거나, 또는 예외가 발생하더라도 실행중이던 인터셉터 이전에 실행된 인터셉터들은 afterCompletion 메소드가 호출된다.

이하 컨트롤러단 또는 postHandle 에서 예외가 발생 하더라도 모든 인터셉터들의 afterCompletion 메소드가 호출된다.

파라미터로 예외를 받기 때문에 예외발생 시 예외정보가 담겨있고 아닐 시 null 을 받는다.

인터셉터 등록

WebMvcConfigurer 를 구현한 클래스에서 addInterceptors 메소들르 오버라이딩 하여 등록이 가능하다.

order 메소드로 각 인터셉터의 우선순위 설정이 가능하다.
preHandle 메소드는 우선순위가 높은 순서대로 호출되고 postHandle, afterCompletion 메소드는 우선순위가 낮은 인터셉터부터 실행된다.

public class DispatcherServlet extends FrameworkServlet {
	...
    ...
    
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// Determine handler for the current request.
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// Determine handler adapter for the current request.
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				String method = request.getMethod();
				boolean isGet = HttpMethod.GET.matches(method);
				if (isGet || HttpMethod.HEAD.matches(method)) {
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}

				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				// Actually invoke the handler.
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}

				applyDefaultViewName(processedRequest, mv);
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new ServletException("Handler dispatch failed: " + err, err);
			}
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new ServletException("Handler processing failed: " + err, err));
		}
		finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			}
			else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}
}


public class HandlerExecutionChain {
	...
    
	private final Object handler;

	private final List<HandlerInterceptor> interceptorList = new ArrayList<>();

	private int interceptorIndex = -1;
    
    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
		for (int i = 0; i < this.interceptorList.size(); i++) {
			HandlerInterceptor interceptor = this.interceptorList.get(i);
			if (!interceptor.preHandle(request, response, this.handler)) {
				triggerAfterCompletion(request, response, null);
				return false;
			}
			this.interceptorIndex = i;
		}
		return true;
	}
    
    void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
			throws Exception {

		for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
			HandlerInterceptor interceptor = this.interceptorList.get(i);
			interceptor.postHandle(request, response, this.handler, mv);
		}
	}
    
    void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
		for (int i = this.interceptorIndex; i >= 0; i--) {
			HandlerInterceptor interceptor = this.interceptorList.get(i);
			try {
				interceptor.afterCompletion(request, response, this.handler, ex);
			}
			catch (Throwable ex2) {
				logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
			}
		}
	}
    
    ...
    ...
}

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
    private final ApiLogInterceptor apiLogInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry
              .addInterceptor(apiLogInterceptor)
			  .order(1)
              .excludePathPatterns("/docs/**")
              .excludePathPatterns("/error")
              .addPathPatterns("/**");
    }
}
profile
주니어 개발자 입니다
post-custom-banner

0개의 댓글