[Spring Web MVC] 예외 처리와 오류 페이지
자바에서 메서드를 실행시킬 때 예외가 발생하면 해당 메서드를 호출한 메서드로 예외를 던지는걸 반복하다가,
메인 메서드에 도달했는데도 예외를 처리하지 못하면 예외 정보를 남기고 해당 쓰레드를 종료시킨다.
스프링 MVC를 사용하는 웹 애플리케이션에서는 쓰레드가 하나만 돌아가지 않는다.
사용자의 요청별로 별도의 쓰레드가 할당되고, 이 쓰레드들은 서블릿 컨테이너 내부에서 실행된다.
예외가 발생했는데 제대로 처리하지 못해 서블릿 컨테이너 외부까지 예외가 전달되면 어떻게 될까?
WAS ←필터 ←디스패처 서블릿 ←인터셉터 ← 컨트롤러 (예외 생김)
차례대로 예외가 전달된다.
그럼 WAS까지 예외가 전달되면?
예외는 서버 내부에서 처리할 수 없는 오류가 발생한 것으로 간주하고 HTTP 상태코드 500번을 반환한다.
그냥 예외를 던지는 경우 말고 sendError() 메서드를 사용하는 경우도 살펴보자.
@GetMapping("/error-ex")
public void errorEx() {
throw new RuntimeException("예외 발생!");
}
@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);
}
HttpServletResponse가 제공하는 sendError메서드를 사용하면 서블릿 컨테이너에게 오류가 발생했다는 사실을 전달할 수 있다. (상태 코드와 오류 메세지도 함께 전달할 수 있다.)
즉, 서블릿에서 예외를 처리하는 방법으로는 Exception 과 sendError 가 있고 서블릿 컨테이너는 클라이언트에게 응답하기 전에 sendError가 호출됐는지 확인하고 호출됐으면 오류 코드에 맞춰 화면을 보여준다.
괜찮은 웹 사이트라면 에러 페이지도 사용자가 보고 한 눈에 알아볼 수 있어야 한다.
기본 오류 화면을 사용하는 대신 사용자 친화적인 오류 화면을 만들어보자.
@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page-/404");
ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page-/500");
ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page-/500");
factory.addErrorPages(errorPage404, errorPage404, errorPageEx);
}
}
오류 종류에 해당하는 ErrorPage 객체를 만들고 factory에 넣어줬다.
@Slf4j
@Controller
public class ErrorPageController {
@RequestMapping("/error-page/404")
public String errorPage404(HttpServletRequest request, HttpServletResponse response){
log.info("404 호출됨");
return "error-page/404";
}
@RequestMapping("/error-page/500")
public String errorPage500(HttpServletRequest request, HttpServletResponse response){
log.info("500 호출됨");
return "error-page/500";
}
}
해당하는 오류들을 처리하는 컨트롤러이다. 오류 페이지에 해당하는 뷰를 호출한다.
특정 지점에서 오류가 발생하면 해당 오류가 WAS까지 호출하고, WAS에서는 ErrorPage를 참고해 해당 URL을 처리하는 컨트롤러를 호출하는 방식으로 동작한다.
WAS ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러 (예외 터짐)
WAS "/error" 다시 요청 → 필터 → 서블릿 → 인터셉터 → 컨트롤러 → 뷰
즉, 서버 내부에서 오류 페이지를 찾기 위해 추가적인 호출이 발생하지만 클라이언트는 서버 내부에서 무슨 일이 일어나는지 알 수 없다.
WAS에서 다시 요청할 때 request의 attribute에 오류 정보를 추가해 주기 때문에 오류 페이지에서 전달된 오류 정보를 보여줄 수 있다.
그런데.. 위의 동작 방식을 보면 알 수 있듯 예외가 발생하면 필터나 인터셉터가 두 번 호출된다.
필터나 인터셉터를 두 번 실행하면 정상적으로 동작하지 않는 웹 페이지가 있을 수 있고, 꼭 그렇지 않더라도 한 번으로 충분한 작업을 두 번 처리하는건 매우 비효율적이기 때문에 방지하는 편이 합리적이다.
서블릿은 클라이언트로부터 발생한 정상 요청인지, 오류 페이지를 출력하기 위한 내부 요청인지를 구분하기 위해 DispatchType 이라는 추가 정보를 제공한다.
클라이언트가 처음 요청하면 DispatchType의 값은 REQUEST이고, 에러가 발생하면 값이 ERROR로 변한다.
REQUEST : 클라이언트 요청
ERROR : 오류 요청
INCLUDE : 서블릿에서 다른 서블릿을 포함할 때
ASYNC : 서블릿 비동기 호출
filterRegistrationBean 에 DispatchType.REQUESST 와 DispatchType.ERROR 를 모두 추가하면 정상 요청과 에러 발생 모두 필터를 작동시키니 유동적으로 설정하자.
인터셉터는 서블릿이 아니라 스프링이 제공하는 기능이기 때문에 DispatcherType과 무관하게 항상 호출된다.
따라서 중복 호출을 제거할 때 필터와는 다른 방식을 사용해야 한다.
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns(
"/css/**", "/*.ico"
, "/error", "/error-page/**" //오류 페이지 경로
);
}
기존에 사용하던 excludePath를 사용해 중복 호출을 제거한다.
예외 처리 페이지를 만들어 클라이언트에게 좀 더 친절한 화면을 제공했는데.. 그 과정이 좀 복잡하다.
예외 종류에 따라 ErrorPage를 추가하고, 예외 처리용 컨트롤러도 따로 만들고 ...
스프링 부트는 이런 귀찮은 과정을 모두 기본으로 제공한다.
/error 경로로 ErrorPage를 자동으로 등록한다.
즉, 서블릿 밖으로 예외가 발생하거나 sendError메서드가 호출되면 /error URL을 호출한다.
스프링 내부에는 BasicErrorController라는 스프링 컨트롤러가 등록돼있고, ErrorPage에서 등록한 /error 경로를 매핑해서 처리해 주기 때문에 개발자는 BasicErrorController가 제공하는 규칙에 따라 오류 페이지 화면만 등록해주면 된다.
1. 뷰 템플릿
resources/template/error/404.html
resources/template/error/4xx.html
2. 정적 리소스
resources/templates/error/404.html
resources/templates/error/4xx.html
3. 뷰 이름
resources/template/error.html (여기서는 error가 뷰 이름)
항상 그렇듯..
404 처럼 구체적인게 4xx 보다 우선순위가 높다.
BasicErrorController는 뷰 템플릿을 호출할 때 모델에 몇 가지 정보를 담는다.
* timestamp: Fri Aug 26 00:00:00 KST 2022
* 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`)
타임리프를 사용해 뷰를 렌더링 할 때 해당 정보를 가져다가 사용할 수 있지만..
오류 관련 내부 정보를 클라이언트에게 보여주는건 좋지 않다.
나쁜 클라이언트는 이 정보를 통해 해킹을 시도할 수 있고, 평범한 클라이언트는 해당 정보를 읽어봤자 무슨 소리인지 전혀 모르기 때문이다.
따라서 BasicErrorController에서 해당 오류 정보를 모델에 포함 여부를 선택해주자.
server.error.include-exception=true
server.error.include-message=on_param
server.error.include-stacktrace=on_param
server.error.include-binding-errors=on_param
(application.properties)
스프링 부트가 기본으로 제공하는 오류 페이지를 사용해 관련 문제를 쉽게 해결할 수 있다..
부트는 내부적으로 DispatchType, Interceptor 등 여러 요소를 사용해 오류 페이지 관련 기능을 지원한다.
'Spring > Spring Web MVC' 카테고리의 다른 글
[Spring Web MVC] 스프링 타입 컨버터 (0) | 2022.08.28 |
---|---|
[Spring Web MVC] API 예외 처리 (0) | 2022.08.26 |
[Spring Web MVC] Bean Validation (0) | 2022.08.23 |
[Spring Web MVC] 데이터 검증 (0) | 2022.08.22 |
[Spring Web MVC] 메세지와 국제화 (0) | 2022.08.20 |
댓글
이 글 공유하기
다른 글
-
[Spring Web MVC] 스프링 타입 컨버터
[Spring Web MVC] 스프링 타입 컨버터
2022.08.28 -
[Spring Web MVC] API 예외 처리
[Spring Web MVC] API 예외 처리
2022.08.26 -
[Spring Web MVC] Bean Validation
[Spring Web MVC] Bean Validation
2022.08.23 -
[Spring Web MVC] 데이터 검증
[Spring Web MVC] 데이터 검증
2022.08.22