예외 처리와 오류 페이지

유동우·2023년 12월 28일
0
post-thumbnail

서블릿 예외 처리 - 시작

스프링의 도움을 받지 않고 순수한 서블릿 컨테이너는 어떻게 예외를 처리할까?

다음 2다기 방식으로 처리한다

  • Exception
  • response.sendError(HTTP 상태 코드, 오류 메시지)

웹 앱은 사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행이 되는데 예외 발생시 try-catch로 잡지 못한 경우 서블릿 밖까지 예외가 튀어나가면 어떻게 될까?

  • WAS(예외 도착) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러 (예외 시작)

이렇게 톰캣과 같은 WAS까지 예외가 전달된다.
일단 스프링 부트가 제공하는 에러페이지 (흔히 whitelabel) 옵션을 꺼두고 시작하자

@Slf4j
@Controller
public class ServletExController {
      @GetMapping("/error-ex")
      public void errorEx() {
		thrownewRuntimeException("예외 발생!");
      }
}

HTTP Status 500 – Internal Server Error

위와같은 톰캣이 기본적으로 제공하는 에러페이지가 나올것이고
위 매핑 말고 다른 url로 요청할 경우 또한 기본 제공하는 404 에러페이지가 나올것이다.

서블릿 컨테이너에게 오류가 발생했다는 점을 전달하고, 오류 메시지를 추가하기 위해 response.sendError 를 사용해보자

//ServletExController 
@GetMapping("/error-404")
public void error404(HttpServletResponse response) throws IOException {
		response.sendError(404, "404 오류!"); }
        
@GetMapping("/error-500")
  public void error500(HttpServletResponse response) throws IOException {
  		response.sendError(500);
}

WAS <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러

위 과정에서 WAS에서 컨트롤러의 sendError를 통해 보낸 에러 메시지를 확인하고 오류 코드에 맞추어 "기본 에러 페이지" 를 보여준다

서비스가 망한것같은 기본 오류페이지 말고 좀더 산뜻한 오류페이지를 만들고 매핑해보자

서블릿 예외처리 - 오류 화면 제공

과거에는 web.xml 파일을 만들어 오류 화면을 등록했지만
이제는 스프링 부트를 통해 서블릿 컨테이너를 실행하기 때문에 좀 더 수월하게 등록할 수 있다

@Component
public class WebServerCustomizer implements 
	WebServerFactoryCustomizer<ConfigurableWebServerFactory>

위 클래스에 에러페이지를 만들어 객체를 생성하고 factory에 등록해준다.

@Slf4j
@Controller
public class ErrorPageController

적절한 컨트롤러를 만들어주고, templates/error-page/OO.html 파일을 만들어 매핑해주자

서블릿 예외처리 - 오류 페이지 작동 원리

http://localhost:8080/error-ex
해당 url을 요청했다고 가정하자

WAS <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러

컨트롤러에서 ("/error-ex") 매핑시 throw new RuntimeException("예외 발생!") 예외를 던지고,
WebServerCustomizer 에서 new ErrorPage(RuntimeException.class, "/error-page/500"); 을 통해 에러페이지를 등록을 해놨기 때문에 error-page/500.html 을 바로 띄우는 것이 아니라 "컨트롤러에 다시 요청" 한다

정리하자면,
컨트롤러에서 발생한 예외가 WAS 까지 전파가 된 후
WAS는 오류 페이지 경로를 찾아 오류 페이지를 호출하는데, 이 과정에서 필터, 서블릿, 인터셉터, 컨트롤러를 모두 재호출한다.

@Slf4j
@Controller
public class ErrorPageController {
	
    public static final String ERROR_EXCEPTION = "javax.servlet.error.exception";
    public static final String ERROR_EXCEPTION_TYPE = "javax.servlet.error.exception_type";
    public static final String ERROR_MESSAGE = "javax.servlet.error.message";
    public static final String ERROR_REQUEST_URI = "javax.servlet.error.request_uri";
    public static final String ERROR_SERVLET_NAME = "javax.servlet.error.servlet_name";
    public static final String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
 ...
}

RequestDispatcher 상수로 정의하고 호출함으로써 에러 정보도 확인이 가능하다

  • javax.servlet.error.exception : 예외
  • javax.servlet.error.exception_type : 예외 타입
  • javax.servlet.error.message : 오류 메시지
  • javax.servlet.error.request_uri : 클라이언트 요청 URI
  • javax.servlet.error.servlet_name : 오류가 발생한 서블릿 이름
  • javax.servlet.error.status_code : HTTP 상태 코드

위 내용을 정리하면서 느낀건데 클라이언트의 요청부터 MVC의 과정에 있어
WAS와 서블릿, 인터셉터 등 자세한 전달 과정을 복기시켜볼 필요가 있을 것 같다

서블릿 예외처리 - 필터

DispatcherType

필터는 재호출 되는 경우를 위해서 dispatcherTypes 라는 옵션을 제공한다.

LogFilter 와 WebConfig를 설정하고 http://localhost:8080/error-ex
위 url을 다시 호출해보자

  1. LogFilter 호출
...

try {
    log.info("REQUEST  [{}][{}][{}]", uuid, request.getDispatcherType(), requestURI);
    chain.doFilter(request, response);
} catch (Exception e) {
      throw e;
} finally {
     log.info("RESPONSE [{}][{}][{}]", uuid, request.getDispatcherType(), requestURI);
}

...

클라이언트의 요청에서는 dispatcherType REQUEST 인 것을 확인할 수 있다.
이후 chain.doFilter를 통해 필터 or 서블릿을 지나

  1. ServletExController의 @GetMappling("/error-ex") 호출

  2. 예외가 발생해서 다시 LogFilter의 catch 문에서 예외 처리

  3. 다시 WAS로 갔다가 로그를 찍고

이번엔 dispatcherType ERROR 인 것을 확인 할 수 있다.

//WebConfig
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR)

WebConfig에 dispatcherType을 설정하지 않으면 기본값은 REQUEST 이다.



필터는 서블릿의 기술이지만 인터셉터는 스프링의 일부이기 때문에 다른 방법이 필요하다
다음 챕터에 이어서 알아보자

서블릿 예외처리 - 인터셉터

인터셉터는 서블릿이 제공하는 것이 아닌 스프링이 제공하는 기능이기 때문에,
DispatcherType과 무관하게 항상 호출 된다.

즉, 필터는 DispatcherType으로 중복 호출을 제거하고 (dispatcherType = REQUEST)
인터셉터는 경로정보로 중복 호출을 제거한다 (excludePathPatterns(" /error-page/** ")

이정도만 간단히 이해하고 넘어가자

스프링 부트 - 오류 페이지1

지금까지 예외 처리 방식을 서블릿으로 근간을 파악했다

  • WebServerConfiguration
  • ErrorPage
  • ErrorPageController

등 매우 귀찮은 작업들을 진행했는데 스프링부트는 이런 기능을 모두 기본으로 제공한다

BasicErrorController 에는 모든 기능이 구현되어 있기 때문에 개발자는 제공되는 룰과 우선순위에 따라 등록만 하면 된다

뷰 템플릿과 정적 리소스 모두 자세한것 -> 포괄적인것 순서로 페이지를 띄운다 (404 가 4xx 보다 우선)

스프링 부트 - 오류 페이지2

BasicErrorController의 기능에 대해 더 알아보자

* timestamp: Fri Feb 05 00:00:00 KST 2021
* status: 400
* error: Bad Request
* exception: org.springframework.validation.BindException 
* trace: 예외 trace
* message: Validation failed for object='data'. Error count: 1 
* errors: Errors(BindingResult)
* path: 클라이언트 요청 경로 (`/hello`)

위 정보를 model에 담아 뷰에 전달하게 된다

<ul>
	<li>오류 정보</li> 
	<ul>
      <li th:text="|timestamp: ${timestamp}|"></li>
      <li th:text="|path: ${path}|"></li>
      <li th:text="|status: ${status}|"></li>
      <li th:text="|message: ${message}|"></li>
      <li th:text="|error: ${error}|"></li>
      <li th:text="|exception: ${exception}|"></li>
      <li th:text="|errors: ${errors}|"></li>
      <li th:text="|trace: ${trace}|"></li>
	</ul>
	</li> 
</ul>

위 뷰 템플릿으로 해당 값들을 출력할 수 있다

오류 관련 정보를 고객에게 노출하는 것은 좋지 않으므로 다음 오류정보들을 model에 포함할지 여부를 선택할 수 있다

//application.properties

server.error.include-exception=true
server.error.include-message=on_param
server.error.include-stacktrace=on_param
server.error.include-binding-errors=on_param

밑에 3개의 오류정보는 기본값이 never이기 때문에 never, always, on_param 등 3개의 속성값을 설정할 수 있다.

Reference
김영한 님 - 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술

profile
효율적이고 꾸준하게

0개의 댓글