서블릿 예외 처리

tokkaiiii·2025년 5월 20일

spring-mvc

목록 보기
23/27

컨트롤러에서 에러를 날리면
ErrorReportValve 클래스로

public void invoke(Request request, Response response) throws IOException, ServletException {
    this.getNext().invoke(request, response);
    if (response.isCommitted()) {
      if (response.setErrorReported()) {
        AtomicBoolean ioAllowed = new AtomicBoolean(true);
        respo

StandardHostValve 클래스에서 invoke 메소드 탐

final class StandardHostValve extends ValveBase {
  private static final Log log = LogFactory.getLog(StandardHostValve.class);
  private static final StringManager sm = StringManager.getManager(StandardHostValve.class);
  private static final ClassLoader MY_CLASSLOADER = StandardHostValve.class.getClassLoader();

  StandardHostValve() {
    super(true);
  }

  public void invoke(Request request, Response response) throws IOException, ServletException {
    Context context = request.getContext();
    if (context == null) {

이 메소드에서

 try {
        context.bind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
        if (asyncAtStart || context.fireRequestInitEvent(request.getRequest())) {
          try {
          // 지금 response가 오류가 난 상태인지 본다
            if (!response.isErrorReportRequired()) {
              context.getPipeline().getFirst().invoke(request, response);
            }
          } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);

가보면 Response 클래스에서

public boolean isErrorReportRequired() {
    return this.getCoyoteResponse().isErrorReportRequired();
  }
// 나는 에러를 던져서 errorState.get()값이 0 이다 그래서 false
public boolean isErrorReportRequired() {
    return this.errorState.get() == 1;
  }

그래서 if문 내부가 실행되고

if (!response.isErrorReportRequired()) {
              context.getPipeline().getFirst().invoke(request, response);
            }

DispatcherServlet에서 doDispatch 함수

    HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());

그리고 handler 찾아서 컨트롤러 실행

 @GetMapping("/exception")
    public String exception() {
        throw new IllegalArgumentException("IllegalArgumentException error");
    }

다시 DispatcherServlet의 doDispatch 로 돌아가서

// 응답을 처리하는 과정에서 예외 발생해서 
 this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
      } catch (Exception ex) {
      // 여기로 이동
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
private static void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception {
    if (mappedHandler != null) {
     // interceptor가 있으면 그쪽에서 예외처리 mappedHandler.triggerAfterCompletion(request, response, ex);
    }
	// 계속 예외를 던짐
    throw ex;
  }

FramworkServlet 클래스에서 processRequest 함수

protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    long startTime = System.currentTimeMillis();
    Throwable failureCause = null;
    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
    LocaleContext localeContext = this.buildLocaleContext(request);
    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
    this.initContextHolders(request, localeContext, requestAttributes);

    try {
      this.doService(request, response);
    } catch (IOException | ServletException ex) {
      failureCause = ex;
      throw ex;
    } catch (Throwable ex) {
      failureCause = ex;
      // 여기서 예외 받아서 다시 던짐
      throw new ServletException("Request processing failed: " + ex, ex);

스프링을 지나서
StandardWrapperValve 클래스로 감
해당 클래스에서 invoke 메소드 타고

 private void exception(Request request, Response response, Throwable exception) {
    this.exception(request, response, exception, 500);
  }

500 상태코드 던짐
Request에 exception 저장

 private void exception(Request request, Response response, Throwable exception, int errorCode) {
    request.setAttribute("jakarta.servlet.error.exception", exception);
    response.setStatus(errorCode);
    response.setError();
  }

StandardHostValve 클래스에서 오류를 꺼냄

 Throwable t = (Throwable)request.getAttribute("jakarta.servlet.error.exception");
// 이번엔 true 다
 if (response.isErrorReportRequired()) {
 
 // 예외가 존재하므로 이 함수탐
  if (t != null) {
                this.throwable(request, response, t);
              } 

보면 에러페이지 존재

protected void throwable(Request request, Response response, Throwable throwable) {
    Context context = request.getContext();
    if (context != null) {
      Throwable realError = throwable;
      if (throwable instanceof ServletException) {
        realError = ((ServletException)throwable).getRootCause();
        if (realError == null) {
          realError = throwable;
        }
      }

      if (realError instanceof ClientAbortException) {
        if (log.isDebugEnabled()) {
          log.debug(sm.getString("standardHost.clientAbort", new Object[]{realError.getCause().getMessage()}));
        }

      } else {
        ErrorPage errorPage = context.findErrorPage(throwable);
        if (errorPage == null && realError != throwable) {
          errorPage = context.findErrorPage(realError);
        }

계속 진행

 else {
          if (response.getStatus() < 400) {
            response.setStatus(500);
          }

          response.setError();
          // 예외 정보로는 오류 페이지를 찾지 못해서 예외 상태로 시도
          this.status(request, response);
        }
private void status(Request request, Response response) {
    int statusCode = response.getStatus();
    Context context = request.getContext();
    if (context != null) {
      if (response.isError()) {
        ErrorPage errorPage = context.findErrorPage(statusCode);
        if (errorPage == null) {
          errorPage = context.findErrorPage(0);

없으면 기본 에러페이지를 얻음

 if (errorPage == null) {
          errorPage = context.findErrorPage(0);
        }

 if (errorPage != null && response.isErrorReportRequired()) {
 // 아직 response 완료되지 않았음을 의미
          response.setAppCommitted(false);

그리고

이 메소드로 오류 관련 정보를 전부 저장
setRequestErrorAttributes

톰캣이 준비한 에러페이지도 있지만 커스텀하게 만들었으면 그 페이지 노출하는 함수

if (this.custom(request, response, errorPage)) {
            response.setErrorReported();

            try {
              response.finishResponse();
            } catch (ClientAbortException var7) {
            } catch (IOException e) {
              this.container.getLogger().warn(sm.getString("standardHostValve.exception", new Object[]{errorPage}), e);
            }
          }

custom 함수를 보면

 RequestDispatcher rd = servletContext.getRequestDispatcher(errorPage.getLocation());
 // 포워딩하는데 스프링이 받음 (BasicErrorController) 
 // 스프링에서 view를 못 찾으면 다시 에러 던짐
 rd.forward(request.getRequest(), response.getResponse());

ErrorReportValve 클래스로 가서 report 함수 실행

protected void report(Request request, Response response, Throwable throwable) {
   int statusCode = response.getStatus();
   if (statusCode >= 400 && response.getContentWritten() <= 0L && response.setErrorReported()) {
     AtomicBoolean result = new AtomicBoolean(false);
     response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
     if (result.get()) {
       ErrorPage errorPage = this.findErrorPage(statusCode, throwable);
       if (errorPage == null || !this.sendErrorPage(errorPage.getLocation(), response)) {
         String message = Escape.htmlElementContent(response.getMessage());
         if (message == null) {
           if (throwable != null) {
             String exceptionMessage = throwable.getMessage();
             if (exceptionMessage != null && exceptionMessage.length() > 0) {
               try (Scanner scanner = new Scanner(exceptionMessage)) {
                 message = Escape.htmlElementContent(scanner.nextLine());
               }
             }
           }

           if (message == null) {
             message = "";
           }
         }

         String reason = null;
         String description = null;
         StringManager smClient = StringManager.getManager("org.apache.catalina.valves", request.getLocales());
         response.setLocale(smClient.getLocale());

         try {
           reason = smClient.getString("http." + statusCode + ".reason");
           description = smClient.getString("http." + statusCode + ".desc");
         } catch (Throwable t) {
           ExceptionUtils.handleThrowable(t);
         }

         if (reason == null || description == null) {
           if (message.isEmpty()) {
             return;
           }

           reason = smClient.getString("errorReportValve.unknownReason");
           description = smClient.getString("errorReportValve.noDescription");
         }

         StringBuilder sb = new StringBuilder();
         sb.append("<!doctype html><html lang=\"");
         sb.append(smClient.getLocale().getLanguage()).append("\">");
         sb.append("<head>");
         sb.append("<title>");
         sb.append(smClient.getString("errorReportValve.s

이건 톰캣이 제공하는 것이고
톰캣 제공하는 화면 보려면

# 이렇게 설정
server.error.whitelabel.enabled=false

스프링에서 제공하는것은
ErrorMvcAutoConfiguration 클래스에서 render 메소드를 탄다

 public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
     if (response.isCommitted()) {
       String message = this.getMessage(model);
       logger.error(message);
     } else {
       response.setContentType(TEXT_HTML_UTF8.toString())
profile
풀스택 자바 개발자입니다

0개의 댓글