2022. 4. 6. 22:47ㆍSpring 기초
서블릿은 크게 두가지 방식으로 예외 처리를 지원한다.
- Exception (예외)
- response.sendError(HTTP 상태 코드, 오류 메시지)
Exception(예외)
자바의 메인 메서드를 실행하면 main이라는 이름의 쓰레드가 실행된다. 실행 도중에 예외 처리를 하지못하고 처음 실행한 main()메서드를 넘어서 예외가 던져지면, 예외 정보를 남기고 해당 쓰레드는 종료된다.
웹 애플리케이션의 경우,
사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다. 애플리케이션에서 예외가 발생했는데, 어디선가 try ~ catch로 예외를 잡아서 처리하면 아무런 문제가 없다. 그런데 만약에 애플리케이션에서 예외를 잡지 못하고, 서블릿 밖으로 까지 예외가 전달되면 어떻게 동작할까? 톰캣 같은 WAS에 까지 예외가 전달된다.
WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
내가 예외 처리를 못하면 예외가 WAS까지 전달되는구나. 그렇담 WAS는 예외가 올라오면 어떻게 되는가?
server.error.whitelabel.enabled=false
웹을 아무 리소스 없이 실행하면 맞닥뜨리는 화이트라벨 페이지는 다들 아실것이다. false로 두어 화이트라벨 페이지는 안보이게 설정하였다.
@Slf4j
@Controller
public class ServletExController {
@GetMapping("/error-ex")
public void errorEx() {
throw new RuntimeException("예외 발생!");
}
}
이러한 상태에서 /error-ex로 요청을 보내면, 예외 처리를 안했으니까 위의 순서대로 컨트롤러에서 WAS까지 예외가 전달 될 것이다. 실행해보면
톰캣이 기본으로 제공하는 오류 화면을 볼 수 있다. (응답코드는 500이다)
Exception 의 경우 서버 내부에서 처리할 수 없는 오류가 발생한 것으로 생각해서 HTTP 상태 코드 500을 반환한다.
/error-ex가 아닌 아무 url 요청을 보내면(예를들어 /hihihi)
HTTP Status 404 – Not Found 톰캣이 기본으로 제공하는 404 오류 화면을 볼 수 있다.(사진은 생략)
당연하지만 오류 발생 시 기본으로 제공하는 페이지는 사용자에게 망한 싸이트인가.. 싶은 인상을 줄 수 있다(사용자 친화적인 페이지를 따로 만들자. 뒤에서 설명~)
response.sendError(HTTP 상태 코드, 오류 메시지)
오류가 발생했을 때 HttpServletResponse 가 제공하는 sendError 라는 메서드를 사용해도 된다. 이것을 호출한다고 당장 예외가 발생하는 것은 아니지만, 서블릿 컨테이너에게 오류가 발생했다는 점을 전달할 수 있다.
이 메서드를 사용하면 HTTP 상태 코드와 오류 메시지도 설정할 수 있다.
@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);
}
response.sendError() 를 호출하면 response 내부에는 오류가 발생했다는 상태를 저장해둔다.
그리고 서블릿 컨테이너는 고객에게 응답 하기전에 response 에 sendError()를 한번 뜯어본다. 만약 sendError()가 호출되었다면 설정한 오류 코드에 맞추어 기본 오류 페이지를 보여준다.
/error-404, /error-500으로 요청을 보내면 HTTP Status 404 – Bad Request
HTTP Status 500 – Internal Server Error(위의 캡쳐 참고) 등 Status만 바꾼 기본 제공 페이지가 등장한다.
이제는 오류가 발생했을때 사용자에게 보여줄 페이지를 설정해보자.
순수 서블릿이 아니라 스프링 부트를 통해 서블릿 컨테이너를 실행하기 때문에, 스프링 부트가 제공하는 기능을 사용해서 서블릿 오류 페이지를 등록한다.
@Component
public class WebServerCustomizer implements
WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/errorpage/404");
//HttpStatus.NOT_FOUND(404)예외가 발생하면, /error-page/404 컨트롤러를 실행시켜! 라고
//해석하면 된다.
ErrorPage errorPage500 = new
ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/errorpage/500");
factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
}
}
errorPageEx: 오류 페이지는 예외를 다룰 때 해당 예외와 그 자식 타입의 오류를 함께 처리한다. 예를 들어서 위의 경우 RuntimeException 은 물론이고 RuntimeException 의 자식도 함께 처리한다.
이제 이 오류 설정 페이지와 짝을 맞출 컨트롤러를 생성하자. HTTPSTATU.NOT_FOUND(404)예외가 발생하면,
/error-page/404 컨트롤러가 실행된다. 그러면 아래의 errorPage404 메서드가 실행되어 error-page/404 View 파일이 반환된다. 그렇다면 예외의 종류에 맞추어 우리가 준비해놓은 view 파일을 보여줄 수 있게되었다.
@Slf4j
@Controller
public class ErrorPageController {
@RequestMapping("/error-page/404")
public String errorPage404(HttpServletRequest request, HttpServletResponse
response) {
log.info("errorPage 404");
return "error-page/404";
}
@RequestMapping("/error-page/500")
public String errorPage500(HttpServletRequest request, HttpServletResponse
response) {
log.info("errorPage 500");
return "error-page/500";
}
}
오류 페이지 요청 흐름
예외 발생 흐름
WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
sendError 흐름
WAS(sendError 호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(response.sendError())
WAS는 해당 예외를 처리하는 오류 페이지 정보를 확인한다.
new ErrorPage(RuntimeException.class, "/error-page/500")
예를 들어서 RuntimeException 예외가 WAS까지 전달되면, WAS는 오류 페이지 정보를 확인한다.
확인해보니 RuntimeException 의 오류 페이지로 /error-page/500 이 지정되어 있다. WAS는 오류
페이지를 출력하기 위해 /error-page/500 를 다시 요청한다.
정리하자면
1. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
2. WAS `/error-page/500` 다시 요청 -> 필터 -> 서블릿 -> 인터셉터
-> 컨트롤러(/error-page/500) -> View
순서이다.
그런데 지금 가만보면,, 필터와 인터셉터를 오갈때마다 거치고있다. 로그인 인증 체크 같은 경우를 생각해보면, 이미 한번 필터나, 인터셉터에서 로그인 체크를 완료했다. 따라서 서버 내부에서 오류 페이지를 호출한다고 해서 해당 필터나 인터셉트가 한번 더 호출되는 것은 매우 비효율적이다.
결국 클라이언트로 부터 발생한 정상 요청인지, 아니면 오류 페이지를 출력하기 위한 내부 요청인지 구분할 수 있어야 한다. 서블릿은 이런 문제를 해결하기 위해 DispatcherType 이라는 추가 정보를 제공한다.
DispatcherType
필터는 이런 경우를 위해서 dispatcherTypes 라는 옵션을 제공한다.
위의 errorPageController에
log.info("dispatchType={}", request.getDispatcherType()) 를 추가하였다.
이 로그를 출력해보면, 오류 페이지에서 dispatchType=ERROR 로 나오는 것을 확인할 수 있다.
고객이 처음 요청하면 dispatcherType=REQUEST 이다. 이렇듯 서블릿 스펙은 실제 고객이 요청한 것인지, 서버가 내부에서 오류 페이지를 요청하는 것인지 DispatcherType 으로 구분할 수 있는 방법을 제공한다.
필터를 등록하는 Configuration 파일에 아래처럼 filter의 setDispatcherTypes를 지정하면 Request뿐만 아니라 Error에도 필터가 작동하게끔 할 수 있다. 아무 설정하지 않으면 기본값은 Request이므로 Error 타입에 필터는 동작하지 않는다. 이렇게 두 가지를 모두 넣으면 클라이언트 요청은 물론이고, 오류 페이지 요청에서도 필터가 호출된다.
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST,
DispatcherType.ERROR)
한편 인터셉터는 이런 설정이 불가능하다. 하지만 url 설정을 보다 세밀하게 할 수 있으므로, 오류 페이지 경로를 excludePathPatterns 를 사용해서 빼주면 된다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns(
"/css/**", "/*.ico",
"/error", "/error-page/**" //오류 페이지 경로
);
}
스프링 부트 - 오류 페이지
지금까지 예외 처리 페이지를 만들기 위해서
WebServerCustomizer 를 만들고
예외 종류에 따라서 ErrorPage 를 추가하고
예외 처리용 컨트롤러 ErrorPageController 를 만들어서 예외 맞춤 View를 클라이언트에게 제공했다.
스프링 부트는 이런 과정을 모두 기본으로 제공한다.
- ErrorPage 를 자동으로 등록한다. 이때 /error 라는 경로로 기본 오류 페이지를 설정한다.
new ErrorPage("/error") , 상태코드와 예외를 설정하지 않으면 기본 오류 페이지로 사용된다.
서블릿 밖으로 예외가 발생하거나, response.sendError(...) 가 호출되면 모든 오류는 /error 를 호출하게 된다. - BasicErrorController 라는 스프링 컨트롤러를 자동으로 등록한다. ErrorPage 에서 등록한 /error 를 매핑해서 처리하는 컨트롤러다.
즉, 오류가 발생했을 때 오류 페이지로 /error 를 기본 요청하면! 스프링 부트가 자동 등록한 BasicErrorController 는 이 경로를 기본으로 받아서 처리하는 방식이다.
BasicErrorController 는 기본적인 로직이 모두 개발되어 있다. 개발자는 오류 페이지 화면만 BasicErrorController 가 제공하는 룰과 우선순위에 따라서 등록하면 된다.
정적 HTML이면 정적 리소스, 뷰 템플릿을 사용해서 동적으로 오류 화면을 만들고 싶으면 뷰 템플릿 경로에 오류 페이지 파일을 만들어서 넣어두기만 하면 된다.
<뷰 선택 우선순위>
BasicErrorController 의 처리 순서
1. 뷰 템플릿
resources/templates/error/500.html
resources/templates/error/5xx.html
2. 정적 리소스( static , public )
resources/static/error/400.html
resources/static/error/404.html
resources/static/error/4xx.html
3. 적용 대상이 없을 때 뷰 이름( error )
resources/templates/error.html
해당 경로 위치에 HTTP 상태 코드 이름의 뷰 파일을 넣어두면 된다. 5xx, 4xx 라고 하면 500대, 400대 오류를 처리해준다!!
예외가 던져지면, 스프링 부트는 해당 상태코드를 기준으로 우선순위에 맞는 View를 반환한다. 웹 에러 페이지는 아주 쉽게 처리가 가능하다.
'Spring 기초' 카테고리의 다른 글
API 예외 처리 (2) - 스프링이 제공하는 ExceptionResolver (0) | 2022.04.09 |
---|---|
API 예외 처리 (1) - MediaType, ExceptionResolver (0) | 2022.04.09 |
로그인 처리 - 필터, 인터셉터(2) (0) | 2022.04.05 |
로그인 처리 - 필터, 인터셉터 (1) (0) | 2022.04.05 |
@SessionAttribute와 세션정보, 세션 타임아웃 설정 (0) | 2022.04.04 |