기본적으로 Spring을 활용하여 웹 서비스 개발을 진행하게 되는데, 서블릿을 통한 예외처리와, Spring에서 제공하는 에러 핸들링 기능을 이용하여 예외 발생 시 어떠한 흐름을 가지고 에러를 핸들링하는지 알아보자.
1. 서블릿의 예외 처리
서블릿의 예외처리 방식은 다음 2가지 방식으로 예외처리를 지원한다.Exception, response.sendError(HTTP 상태코드, 오류 메시지)
1-1. Exception
웹 애플리케이션 환경에서는 사용자 요청별로 별도의 쓰레드가 할당이 되는데, 서블릿 컨테이너에 안에서 관리되며 실행된다.
애플리케이션에서 예외가 발생했는데 만약 어디선가 try~catch
로 예외를 잡아서 처리하면 아무런 문제가 없다.
그런데 웹 애플리케이션 환경에서 예외를 잡지 못한 경우, 서블릿 밖으로까지 예외가 전달되게 되는데 애플리케이션에서 에러를 잡지 못할 경우 WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
흐름으로 에러가 WAS까지 전파된다.
결국 톰캣 같은 WAS까지 예외가 전달되는데, WAS는 밑단에서 올라온 예외를 어떻게 처리할까?
WAS가 예외를 어떻게 처리하는지 보기전에 스프링 부트가 제공하는 기본 예외 페이지가 있다(Whitelable page
) 이거를 먼저 꺼두고 보자.
@Slf4j
@Controller
public class ServletExController {
@GetMapping("/error-ex")
public void errorEx() {
throw new RuntimeException("예외 발생!"); }
}
실행해보면 tomcat이 기본으로 제공하는 화면을 볼 수 있다.
HTTP Status 500 – Internal Server Error
원래는 기존에 스프링부트가 보여주던 오류 페이지인 whitelabel
오류 페이지가 나왔었다.
그러나 그것을 false
로 두고, 실행 해보니 tomcat
이 기본으로 제공하는 오류 화면을 볼 수 있다.
서블릿은 예외가 발생하게 되면 WAS까지 올라가서 해당 오류 페이지 화면을 보여준다.
1-2. response.sendError(Http상태코드,오류 메시지)
만약 서블릿을 통해 Exception
을 처리하고 싶다면 HttpServletResponse
의 sendError
메서드를 활용하면 된다.
해당 메서드를 활용하여 예외가 발생하게되면 sendError
메서드를 호출하여 인자를 읽어서, Servlet의 상태코드와 오류 메시지를 읽어들인다.
@Slf4j
@Controller
public class ServletExController {
@GetMapping("/error-ex")
public void errorEx() {
response.sendError(HttpServletResponse.SC_NOT_FOUND,"404 Error!!");
}
}
sendError 메서드 호출 시의 흐름WAS(sendError 호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(response.sendError())
작동 원리
에러 발생 시 response.sendError를 호출하게 되는데 이 때 response 내부에는 오류가 발생했다는 상태를 저장한다.
그리고 서블릿 컨테이너는 응답을 반환하기전에 sendError가 호출 되었는지 호출 기록을 확인 한 후, 호출되었다면(예외가 발생했다면) 설정한 오류 상태 코드에 맞추어 오류 페이지를 보여준다.
Exception
이 터질 경우 Servlet컨테이너는 500 error
가 터진다. 그게 아닐 경우, 내가 직접 HTTP 상태코드랑 에러메시지를 담고싶다 그러면 response.sendError
를 던지면 된다.
2. 서블릿의 예외처리 화면 다듬기
위에서 살펴본 Exception(WAS의 기본 오류 화면
), respnse.sendError를 활용하여 서블릿에서 에러의 상태코드를 가지고 에러 페이지를 웹 브라우저에 반환하는 방법까지 살펴봤다.
response.sendError를 활용하여 서블릿에서 Exception을 좀 더 보기좋게 다듬을 수 있었지만 친화적이지 않은 에러 페이지이다.
WebServerFactoryCustomizer
를 활용하여 서블릿의 에러를 고객친화적으로 다듬을 수 있다.
@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
@Override
public void customize(ConfigurableServletWebServerFactory 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, errorPage500, errorPageEx);
}
response.sendError(404)
:errorPage404
호출response.sendError(500)
:errorPage500
호출RuntimeException
또는 그 자식 타입의 예외:errorPageEx
호출
오류 페이지는 예외를 다룰 때 해당 예외와 그 자식 타입의 오류를 함께 처리한다. 예를 들어서 위의 경우 RuntimeException
은 물론이고 RuntimeException
의 자식도 함께 처리한다.
@Slf4j
@Controller
public class ErrorPageController {
//RequestDispatcher 상수로 정의되어 있음
public static final String ERROR_EXCEPTION = "javax.servlet.error.exception";
public static final String ERROR_EXCEPTION_TYPE = "javax.servlet.error.exception_type";
//생략...
@RequestMapping("/error-page/404")
public String errorPage404(HttpServletRequest request, HttpServletResponse response) {
log.info("errorPage 404");
printErrorInfo(request);
return "error-page/404";
}
@RequestMapping("/error-page/500")
public String errorPage500(HttpServletRequest request, HttpServletResponse response) {
log.info("errorPage 500");
printErrorInfo(request);
return "error-page/500";
}
private void printErrorInfo(HttpServletRequest request) {
log.info("ERROR_EXCEPTION: {}", request.getAttribute(ERROR_EXCEPTION));
log.info("ERROR_EXCEPTION_TYPE: {}", request.getAttribute(ERROR_EXCEPTION_TYPE));
log.info("ERROR_MESSAGE: {}", request.getAttribute(ERROR_MESSAGE));
// 생략...
}
}
RequestDispatcher
클래스를 활용하면 Error의 상수를 가져올 수 있다.
이렇게 등록해주면 요청 시 에러가 발생할 경우 설정한 에러페이지 경로를 재호출함으로써 내가 직접 정의하 에러페이지를 보여줄 수 있게 핸들링 할 수 있다.
작동 원리
서블릿은 Exception
(예외)가 발생해서 서블릿 밖으로 전달되거나 또는 response.sendError()
가 호출 되었을 때 설정된 오류 페이지를 찾는다.
중요한 점은 웹 브라우저(클라이언트)는 서버 내부에서 이런 일이 일어나는지 전혀 모른다는 점이다. 오직 서버 내부에서 오류 페이지를 찾기 위해 추가적인 호출을 한다.
- Exception 발생 시 WAS까지 전파된다.
- sendError로 전파 시에는 WAS는 오류 페이지 경로를 찾아서 내부에서 오류 페이지를 처리하는 메서드를 재 호출한다. (한번의 요청에 에러 발생시 2번을 왔다갔다 하는 것)
-> 즉 오류가 발생하면서 오류페지이를 출력하기 위해WAS
내부에서 다시 한번 호출이 발생하게 된다. 이때 필터, 서블릿, 인터셉터도 모두 다시 호출된다.
2-1 필터 및 인터셉터 적용하기
예외를 처리하기 위해 response.sendError 메서드를 활용하여 에러 발생 시 에러 페이지를 렌더링 할 수 있도록 해놨다.
이럴 경우 만약 한번의 요청에서 컨트롤러에서 예외가 터질 경우, WAS까지 전파되는데 sendError를 정의해놨다면 sendError의 경로로 재호출하게 된다.
그래서 서블릿은 이러한 비효율적인 문제를 해결하기 위해 DispatcherType
이라는 추가 정보를 제공한다.
DefulatValue는 REQUEST이며 에러 처리 요청은 ERROR이다.
필터는 서블릿 기술이므로 필터 등록(FilterRegistrationBean) 시에 호출될 dispatcherType 타입을 설정할 수 있고, 별도의 설정이 없다면 REQUEST일 경우에만 필터가 호출된다. 하지만 인터셉터는 스프링 기술이므로 dispatcherType을 설정할 수 없어 URI 패턴으로 처리가 필요하다.
@Slf4j public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
try {
// request.getDispatcherType() 추가
log.info("REQUEST [{}][{}][{}]", uuid, request.getDispatcherType(),requestURI);
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
} finally {
log.info("RESPONSE [{}][{}][{}]", uuid, request.getDispatcherType(),
}
}
로그를 출력하는 부분에 request.getDispatcherType()
을 추가해두었다.
이제 Filter를 Bean으로 등록해주자.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new
FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
//DispatcherType을 사용하여, Request와 Error를 설정해줌.
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST,
DispatcherType.ERROR);
return filterRegistrationBean;
}
}
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST,DispatcherType.ERROR)
이렇게 두 가지를 모두 넣으면 클라이언트 요청은 물론이고, 오류페이지 요청에서도 필터가 호출된다. 아무것도 넣지 않으면 기본값은 DispatchType.REQUEST
이다.
정리
즉 클라이언트의 요청이 있는 경우에만 필터가 적용된다. 특별히 오류 페이지 경로도 필터를 적용할 것이 아니면, 기본 값을 그대로 사용하면 된다. 물론 오류 페이지 요청 전용 필터를 적용하고 싶으면 DispatcherType.ERROR
만 지정하면 된다.
Servlet의 sendError()
흐름
/hello
정상 요청
-> WAS(/hello, dispatchType=REQUEST) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 -> View/error-ex
오류 요청
-> 필터는DispatchType
으로 중복 호출 제거 (dispatchType=REQUEST
)- DispatchType 적용 시
WAS(/error-ex, dispatchType=REQUEST) -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(예외발생) -> 인터셉터 -> 서블릿 -> 필터 -> WAS(오류 페이지 확인) -> WAS(/error-page/500, dispatchType=ERROR) -> 필터(x) -> 서블릿 -> 인터셉터(x) -> 컨트롤러(/error-page/500)
1.스프링 부트가 제공하는 오류페이지
위에서 보았듯이 예외 페이지를 만들기 위해서 다음과 같은 과정을 거쳤다.
sendError를 호출하는데 사용자 친화적으로 만들기 위해서 Customizer를 통해 커스텀하고,한번의 예외에 2번 계층을 왔다갔다하는 비효율 때문에 DispatcherType을 사용하였다.
해당 방식은 Servlet을 이용할 때이며, SpringBoot가 만들어주는 예외 페이지를 이용하여 에러 페이지를 핸들링 해보자.
1-1.스프링 부트는 이런 과정을 모두 기본으로 제공해준다.
Servlet의 Exception 상황에서는 ErrorPage의 경로를 제공해주었었다.
허나 스프링부트는 ErrorPage를 자동으로 등록하는데 경로는 /error
라는 경로로 기본 오류 페이지를 설정한다.
- 서블릿 밖으로 예외가 발생하거나,
response.sendError(...)
가 호출되면 모든 오류는/error
를호출하게 된다. new ErrorPage("/error")
, 상태코드와 예외를 설정하지 않으면 기본 오류 페이지로 사용된다.BasicErrorController
라는 스프링 컨트롤러를 자동으로 등록한다. -> ErrorPage에서 등록한 /error 를 처리하는 컨트롤러다.
BasicErrorContrller
정적 HTML이면 정적 리소스, 뷰 템플릿을 사용해서 동적으로 오류 화면을 만들고 싶으면 뷰 템플릿 경로에 오류 페이지 파일을 만들어서 넣어두기만 하면 된다.
뷰 템플릿이 정적 리소스보다 우선순위가 높고, 404, 500처럼 구체적인 것이 5xx처럼 덜 구체적인 것보다 우선순위가 높다.
(우선순위가 존재한다.)5xx,4xx라고하면 500대, 400대 오류를 처리해준다.
BasicErrorController 컨트롤러는 다음 정보를 model에 담아서 뷰에 전달한다. 뷰 템플릿은 해당 값을 활용해서 출력할 수 있다.
에러메시지 활용하기
application.properties 파일에 이러한 에러미시지를 활용할 수 있다.
- server.error.include-exception=false : exception 포함 여부( true , false )
- server.error.include-message=never : message 포함 여부
- server.error.include-stacktrace=never : trace 포함 여부
- server.error.include-binding-errors=never : errors 포함 여부
server.error.include-exception=true
server.error.include-message=on_param
server.error.include-stacktrace=on_param
server.error.include-binding-errors=on_param
기본 값이 never 인 부분은 다음 3가지 옵션을 사용할 수 있다.
never, always, on_param
never : 사용하지 않음
always :항상 사용
on_param : 파라미터가 있을 때 사용
on_param 은 파라미터가 있으면 해당 정보를 노출한다. 디버그 시 문제를 확인하기 위해 사용할 수 있다. 그런데 이 부분도 개발 서버에서 사용할 수 있지만, 운영 서버에서는 권장하지 않는다.
on_param 으로 설정하고 다음과 같이 HTTP 요청시 파라미터를 전달하면 해당 정보들이 `model` 에 담겨서 뷰 템플릿에서 출력된다.
- timestamp: Fri Feb 05 00:00:00 KST 2021
- status: 400
- error: Bad Request
- exception: org.springframework.validation.BindException * trace: 예외 trace
- path: 클라이언트 요청 경로 (/hello)
BasicErrorController덕분에 예외 발생 시 뷰 템플릿으로 에러의 내용을 모델에 담아서 처리할 수 있게 되었다.
BasicErrorController 를 확장하면 JSON 메시지도 변경할 수 있다. 그런데 API 오류는 조금 뒤에 설명할 @ExceptionHandler 가 제공하는 기능을 사용하는 것이 더 나은 방법이므로 지금은 BasicErrorController 를 확장해서 JSON 오류 메시지를 변경할 수 있다 정도로만 이해해두자.
BasicErrorController가 기본적으로 에러페이지를 생성해주고 에러를 관리해준다는 것을 알았다.
참고ErrorMvcAutoConfiguration
이라는 클래스가 오류 페이지를 자동으로 등록하는 역할을 한다.
1-2. @ControllerAdvice, @ExceptionHandler, ExceptionResolver
ExceptionResolver
발생하는 예외에 따라서 400, 404 등등 다른 상태코드로 처리하고 싶다면 ExceptionResolver를 사용하면 된다.
스프링 MVC는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공한다.
컨트롤러 밖으로 던져진 예외를 해결하고, 동작 방식을 변경하고 싶으면HandlerExceptionResolver
를 사용하면 된다. 줄여서 ExceptionResolver
라 한다.
public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex);
}
HandlerExceptionResolver 구현하기
@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
try {
if (ex instanceof IllegalArgumentException) {
log.info("IllegalArgumentException resolver to 400");
response.sendError(HttpServletResponse.SC_BAD_REQUEST,
ex.getMessage());
return new ModelAndView();
}
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
}
ExceptionResolver
가 ModelAndView
를 반환하는 이유는 마치 try, catch를 하듯이, Exception
을 처리해서 정상 흐름 처럼 변경하는 것이 목적이다. 이름 그대로 Exception
을 Resolver(해결)하는 것이 목적이다.
여기서는 IllegalArgumentException
이 발생하면 response.sendError(400)
를 호출해서 HTTP 상태 코드를 400으로 지정하고, 빈 ModelAndView
를 반환한다.
반환 값에 따른 동작 방식HandlerExceptionResolver
의 반환 값에 따른 DispatcherServlet
의 동작 방식은 다음과 같다.
- 빈 ModelAndView:
new ModelAndView()
처럼 빈ModelAndView
를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다. - ModelAndView 지정:
ModelAndView
에View
,Model
등의 정보를 지정해서 반환하면 뷰를 렌더링 한다. - null:
null
을 반환하면, 다음ExceptionResolver
를 찾아서 실행한다. 만약 처리할 수 있는ExceptionResolver
가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던진다.
ExceptionResolver 활용
- 예외 상태 코드 변환
- 예외를
response.sendError(xxx)
호출로 변경해서 서블릿에서 상태 코드에 따른 오류를 처리하도록 위임 - 이후 WAS는 서블릿 오류 페이지를 찾아서 내부 호출, 예를 들어서 스프링 부트가 기본으로 설정한
/error
가 호출된다.
- 예외를
- 뷰 템플릿 처리
ModelAndView
에 값을 채워서 예외에 따른 새로운 오류 화면 뷰 렌더링 해서 고객에게 제공한다.
@ControllerAdvice
@ControllerAdvice
란 Spring의 AOP 개념을 이용한 것이다.
AOP란 웹 프로젝트 개발에 있어서 공통된 관심사를 메인 메서드에서 분리한 채 따로 적용하자는 개념이다.
이를 예외처리에 맞게 설명하자면 공통된 로직, 중복되는 로직이 있다 가정해보자, 각 메서드마다 예외 처리를 해주기 위해서 try~catch문이 적용되어있다.
메서드가 한두개면 상관없는데 프로젝트의 규모가 너무 클 경우 너무 벅찬작업이다. 그래서 이것을 메인 로직에서 분리하고 따로보자 라는 개념으로 나온것이 AOP라는 것.
그래서 ControllerAdvice
는 예외처리를 공통된 로직을 묶어서 전역적으로 처리한다.
스프링 부트의 @ControllerAdvice,@ExceptionHandler 예외 발생 시 의 흐름
요청을 처리하는 도중 Controller에서 예외가 발생할 경우 먼저 에러가 발생한 해당 Controller의 ExceptionHandler가 작동하게 된다. 근데 ExcpetionHandler를 찾지 못한 경우, ControllerAdvice를 찾아서 실행한다.
ControllerAdvice에서도 ExcpetionHandler를 찾지 못한 경우에는, ExcpetionResolver로 등록된 Bean들을 찾게 된다.
ExcpetionResolver를 찾지 못했을 경우에 BasicErrorController가 에러를 핸들링해서 DispatcherServlet을 통해 WAS로 응답이 반환된다.
1.ExceptionHandler
2.ControllerAdvice - ExceptionHandler
3.ExceptionResolver를 통해 등록된 Bean 조회
4.BasicErrorController
@ExceptionHandler(ViewException.class)
public ModelAndView ex(ViewException e) {
log.info("exception e", e);
return new ModelAndView("error");
}
다음과 같이 ModelAndView를 반환 함으로써 HTML를 렌더링 하는데 사용가능하다.
전역적으로 지원하고 싶지 않을 때
1. 클래스 레벨에서 애노테이션으로 적용시키는 방법
Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
2.ControllerAdvice 애노테이션에 패키지 주소를 적용시키는 방법, 패키지 주소 포함 그 하위 클래스들에게 ControllerAdvice를 적용시킨다.
Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
3. 클래스 명을 명시해줌으로써 적용시키는 방법
Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class,
AbstractController.class})
스프링 공식 문서 예제에서 보는 것 처럼 특정 애노테이션이 있는 컨트롤러를 지정할 수 있고, 특정 패키지를 직접 지정할 수도 있다. 패키지 지정의 경우 해당 패키지와 그 하위에 있는 컨트롤러가 대상이 된다. 그리고 특정 클래스를 지정할수도 있다.
예를 들어 상품과 관련된 컨트롤러의 예외처리, 주문과 관련된 컨트롤러의 예외처리를 따로해주고 싶은 경우, 애노테이션을 따로 만든다거나 패키지 명을 적어주면 된다.
정리
자바와 스프링부트를 이용하여 예외처리를 하게 된다면 이와같은 방식으로 하게 될텐데, 스프링없이 Servlet으로 예외 처리를 하는 방식과, 스프링을 이용하여 예외처리를 활용하는 방식을 살펴보았다 그리고 ConterollerAdvice,ExceptionHandler를 통해 API 예외도 처리할 수 있다. 그리고 어떤식으로 동작하는지 살펴보았다.
'스프링' 카테고리의 다른 글
[Spring] 스프링이 DB 기술에 따라 예외를 제공하는 방법 (0) | 2024.07.13 |
---|---|
[Spring] RuntimeException(UncheckExcpetion) 활용하기 (0) | 2024.07.12 |
[Spring] 서블릿의 Filter, 스프링의 Interceptor 예제 코드로 알아보기 (0) | 2024.05.16 |
[Spring] 로그인 기능 구현으로 알아보는 쿠키 및 세션 (2) | 2024.05.13 |
[Spring] 애노테이션 파라미터를 처리하는 ArgumentResolver (1) | 2024.05.13 |