예외 처리 흐름

요청 → WAS(톰캣) → 필터 → 서블릿 → 인터셉터 → 컨트롤러(예외 발생)
컨트롤러 → 인터셉터 → 서블릿 → 필터 → WAS (예외 전달)
WAS /error 재요청 → 필터 → 서블릿 → 인터셉터 → BasicErrorController → 인터셉터 → 뷰 리졸버 호출
뷰 리졸버 → 인터셉터 → 서블릿 → 필터 → WAS → 응답

컨트롤러에서 예외가 발생하여 필터나 인터셉터에서 처리하지 못하여 WAS(톰캣)까지 전달되면 위의 흐름이 발생한다.

WAS까지 예외가 전달되면 WAS에서는 Spring에게 GET /error 를 재요청하게 된다. 때문에 클라이언트의 요청과 동일하게 필터와 인터셉터를 거치게되며 최종적으로 해당 요청을 처리하는 컨트롤러에게 전달되게 된다.

이 요청을 처리하기 위한 기본적인 컨트롤러는 BasicErrorController라는 이름으로 작성되어 있으며, 클라이언트의 Accpet-Header에 따라 ViewResolver를 호출할지, HttpMessageConverter를 호출할지 정해놓았다.

별도의 예외 처리를 위한 뷰 템플릿을 제작하고 싶다면, Controller를 손수 만드는 것 보다 후술할 기본적으로 제공하는 BasicErrorController의 뷰 선택 우선 순위에 따라 파일을 만드는 것만으로 손쉽게 자신만의 뷰 템플릿을 클라이언트에게 제공할 수 있다.

ErrorMvcAutoConfiguration

package org.springframework.boot.autoconfigure.web.servlet.error;

@AutoConfiguration(before = WebMvcAutoConfiguration.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration { ... }

스프링의 기본 예외 처리를 설정하는 설정 클래스이다.
기본적으로 BasicErrorController를 호출하도록 설정되어 있다.

BasicErrorController

package org.springframework.boot.autoconfigure.web.servlet.error;

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController { 

    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(
                HttpServletRequest request, 
                HttpServletResponse response
    ) { ... }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(
                HttpServletRequest request
    ) { ... }
}
  • @RequestMapping("${server.error.path:${error.path:/error}}")
    • application.properties에 sever.error.path를 찾는다.
    • 없으면, error.path를 찾는다.
    • 그래도 없으면, /error 를 사용한다.
  • @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    • Accept-Header에 text/html이 포함되어 있으면 errorHtml 메서드를 실행한다.
    • 그 외의 경우엔 error 메서드를 실행한다.

BasicErrorController의 뷰 선택 우선 순위

  • 뷰 템플릿
    • resources/templates/error/500.html
    • resources/templates/error/5xx.html
  • 정적 리소스
    • resources/static/error/400.html
    • resources/static/error/404.html
    • resources/static/error/4xx.html
  • 기본 뷰 템플릿
    • resources/templates/error.html

예외에 따라 자신만의 뷰 템플릿을 클라이언트에게 보여주고 싶으면 위에 기술된 경로에 해당 파일을 작성하면 된다. 위에 적힌 순서에 따라 파일을 찾게 되며, 아무것도 작성하지 않으면 Spring Boot가 제공하는 Whitelabel Error Page를 표시하게 된다.

해당 응답 코드에 따라 개별적인 파일을 작성하여 핸들링이 가능하다. 500, 404 처럼 작성하면 그에 맞는 응답 코드만 핸들링이 되고, 5xx, 4xx 으로 작성하면 500번대, 400번대 전체의 응답 코드가 핸들링이 된다.

DispatcherType

package org.springframework.boot.web.servlet;

public enum DispatcherType {
    // 다른 서블릿이나 JSP를 호출할 때
    // RequestDispatcher.forward(request, response);
    FORWARD,
    
    // 다른 서블릿이나 JSP의 결과를 포함할 때
    // RequestDispatcher.include(request, response);
    INCLUDE,
    
    // 클라이언트 요청
    // GET /info
    REQUEST,
    
    // 비동기 호출
    ASYNC,
    
    // 오류 요청
    // throw
    // response.sendError()
    ERROR
}
Type설명예시
REQUEST클라이언트 요청GET /info
ERROR오류 요청response.sendError()
FORWARD다른 서블릿이나 JSP를 호출할 때RequestDispatcher.forward(request, response)
INCLUDE다른 서블릿이나 JSP의 결과를 포함할 때RequestDispatcher.include(request, response)
ASYNC서블릿 비동기 호출-

클라이언트에게 요청받은 WAS는 서블릿에게 전달하기 전에 해당 요청의 특징이 무엇인지를 DispatcherType의 형태로 저장하여 전달하게 된다. 이는 ServletRequest.getDispatcherType() 메서드로 확인할 수 있다.

이를 이용해 필터나 인터셉터에서 로직을 실행하기 전에 검사하여 그에 알맞는 처리를 할 수 있게 된다.

RequestDispatcher

package jakarta.servlet;

public interface RequestDispatcher {
    // ...
    public static final String ERROR_EXCEPTION = "javax.servlet.error.exception";
    public static final String ERROR_EXCEPTION_TYPE = "jakarta.servlet.error.exception_type";
    public static final String ERROR_MESSAGE = "jakarta.servlet.error.message";
    public static final String ERROR_REQUEST_URI = "jakarta.servlet.error.request_uri";
    public static final String ERROR_SERVLET_NAME = "jakarta.servlet.error.servlet_name";
    public static final String ERROR_STATUS_CODE = "jakarta.servlet.error.status_code";
    // ...
}

공식 Docs

WAS는 에러 페이지를 재호출할때 여러 정보를 Request.attribute에 담아서 전달한다.

그 정보들은 ServletRequest.getAttribute, ServeltRequest.getRequestDispatcher 메서드를 통해 값을 확인할 수 있으며, RequestDispatcher는 그에 해당하는 키값을 저장하고 있다.

  • request.getAttribute(RequestDispatcher.ERROR_EXCEPTION)

위에 예시로 작성된 값 이외에도 굉장히 많은 값이 지정되어 있으니 필요하면 공식 Docs를 통해 찾아보자.

변수 이름설명
ERROR_EXCEPTION컨트롤러에서 발생한 예외 (Throwable)
ERROR_EXCEPTION_TYPE컨트롤러에서 발생한 예외 타입 (Class)
ERROR_MESSAGE컨트롤러에서 발생한 예외 메시지 (String)
ERROR_REQUEST_URI클라이언트에서 요청한 원본 URI (String)
ERROR_SERVLET_NAME오류가 발생한 서블릿 이름 (String)
ERROR_STATUS_CODEHTTP 상태 코드 (Integer)

application.properties

# Exception 포함 여부. (true, false)
# 기본값: false
server.error.include-exception = true

# message 포함 여부. (never, always, on_param)
# on_param의 경우, URL에 ?message 를 추가하면 표시가 가능하다.
# 기본값: never
server.error.include-message = on_param

# stacktrace 포함 여부. (never, always, on_param)
# on_param의 경우, URL에 ?trace 를 추가하면 표시가 가능하다.
# 기본값: never
server.error.include-stacktrace = on_param

# errors 포함 여부. (never, always, on_param)
# on_param의 경우, URL에 ?errors 를 추가하면 표시가 가능하다.
# 기본값: never
server.error.include-binding-errors = on_param

# 기본 스프링 부트 whitelabel 오류 페이지 적용. (true, false)
# 기본값: true
server.error.whitelabel.enabled = true

# 오류 페이지 경로
# 스프링이 자동 등록하는 "서블릿 글로벌 오류 페이지 경로"와 "BasicErrorController" 오류 컨트롤러 경로에 함께 사용된다.
# 기본값: /error
server.error.path = /error

공식 Docs

Spring에서는 에러 처리에 관한 다양한 설정값을 설정할 수 있도록 지원한다.
운영 환경에서는 기본값(never)으로 설정하여 클라이언트에게 예외에 대한 설명을 제공하지 않는 것이 좋다.

profile
백엔드 개발자 지망생

0개의 댓글