반응형
스프링 부트 프로젝트를 하나 띄워서 없는 주소로 요청하면 다음과 같은 예외가 발생한다.
이 응답은 어디서 온걸까?
서블릿의 예외처리 과정
Exception을 던지기
response.sendError 메서드 호출
- 메서드 파라미터로 HTTP 상태 코드와 에러메시지 전달 가능
- 서블릿 컨테이너는 오류 코드에 적절한 오류 페이지 제공
에러페이지 작동 원리
- 컨트롤러에서 예외가 발생할 경우 컨트롤러 → 인터셉터 → 서블릿 → 필터 → WAS 순으로 예외가 전달되면 WAS는 에러 페이지 정보를 확인하고 다시 에러 페이지 출력을 위해 재요청
- HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 - 예외 발생 -> 인터셉터 -> 서블릿 -> 필터 -> WAS - 예외 페이지 정보 확인 -> 필터 -> 서블릿 -> 인터셉터 -> 예외 페이지 컨트롤러 -> 예외 페이지 (View)
- 클라이언트는 이러한 프로세스를 한번 더 타는 것을 모름 (내부에서만 추가적인 호출을 한다)
필터
- 필터를 두 번 호출하는 것은 비효율적..
- 결국 필터가 호출될 때 정상적인 요청에 의한 호출인지 에러 페이지 호출에 따른 호출인지 판별 가능해야 한다
- DispatcherType REQUEST, ERROR
- 어떤 dispatcherType일 때만 호출할 지 정의해줄 수 있다.
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST
, DispatcherType.ERROR); // HTTP 요청 및 에러인 경우에 호출됨
return filterRegistrationBean;
}
// 로그인 필터는 REQUEST일 때만 등록해준다.
BasicErrorController
spring boot는 기본적으로 BasicErrorController를 사용해서 모든 에러 처리를 하고 있다.
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
...
- property에 server.error.path가 있으면 그 값을 없을경우 error.path를 사용하고 없는 경우 /error를 맵핑한다.
HTML 응답
Whitelabel Error Page가 나온다
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
- Request 헤더의 Accept 속성 값이 text/html인 경우 다음 코드를 통해 오류페이지를 뷰로 반환해주고 있다.
Json 응답
{
"timestamp": "2021-04-25T03:08:18.754+00:00",
"status": 404,
"error": "Not Found",
"message": "",
"path": "/api/asd"
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
- error메서드에서 처리하여 응답값을 내려주고, 응답값은 getErrorAttributes 메서드에서 채워준다.
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
...생략...
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap();
errorAttributes.put("timestamp", new Date());
this.addStatus(errorAttributes, webRequest);
this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
this.addPath(errorAttributes, webRequest);
return errorAttributes;
}
}
Custom Error 페이지에 대한 처리
Error 관련 Properties
# spring boot의 기본 properties
server.error:
include-exception: false
include-stacktrace: never # 오류 응답에 stacktrace 내용을 포함할 지 여부
path: '/error' # 오류 응답을 처리할 Handler의 경로
whitelabel.enabled: true # 서버 오류 발생시 브라우저에 보여줄 기본 페이지 생성 여부
error.include-exception : 응답에 exception 내용을 포함할지
error.include-stacktrace : 응답에 stacktrace 내용을 포함할지
error.path : 오류 응답 처리할 핸들러(ErrorController)의 path
error.whitelabel.enabled : 브라우저 요청에 대해 서버 오류시 기본적으로 노출할 페이지를 사용할지 여부
Custom ErrorController 생성하기
@Controller
class CustomErrorController(errorAttributes: ErrorAttributes, private val serverProperties: ServerProperties) :
AbstractErrorController(errorAttributes) {
@RequestMapping("/error")
fun error(request: HttpServletRequest): ResponseEntity<Map<String, String>> {
val status = getStatus(request)
if (status == HttpStatus.NO_CONTENT) {
return ResponseEntity.noContent().build()
}
val attributes = getErrorAttributes(request, ErrorAttributeOptions.defaults())
val responseBody = mapOf(
"uri" to attributes["path"]?.toString().orEmpty(),
"customMessage" to attributes["message"]?.toString().orEmpty()
)
return ResponseEntity(responseBody, status)
}
override fun getErrorPath(): String {
return serverProperties.error.path
}
}
커스텀 하는 방법이다.
custom error page
src/
+- main/
+- java/
| + <source code>
+- resources/
+- templates/
+- error/
| +- 404.html
| +- 5xx.html
+- <other public assets>
ExceptionHandler
컨트롤러 레벨에서 익셉션 처리
public class FooController{
//...
@ExceptionHandler({ CustomException1.class, CustomException2.class })
public void handleException() {
//
}
}
하지만 이것은 특정 Controller에만 설정을 할 수 있다. (ControllerAdvice를 사용하지 않으면) 물론, 모든 컨트롤러가 Base Contorller를 상속하는 식으로 구현할 수도 있긴 하다.
HandlerExceptionResolver
어떤 Exception이라도 처리가 가능하다.
- ExceptionHandlerExceptionResolver
- ExceptionHandler를 통한 전역 에러 처리는 얘가 담당
- DefaultHandlerExceptionResolver
- HTTP Status Code에 대응하는 Exception 처리 예 ) 4XX, 5XX
- 메소드를 찾을 수 없는 경우에는 NoSuchRequestHandlingMethodException 예외가 발생한다. 이 예외에 대해서는 HTTP 404 - Not Found 로 응답 상태를 지정
- 타입이 일치하지 않을 때 발생하는 TypeMismatchException 은 HTTP 400 - Bad Request 응답 상태로 돌려주는 식
- 상태 코드는 적절하게 설정하지만, Response body에는 아무것도 하지 않는다는 단점
- HTTP Status Code에 대응하는 Exception 처리 예 ) 4XX, 5XX
Spring 3.2부터는
- @ControllerAdvice
@ControllerAdvice
public class RestResponseEntityExceptionHandler
extends ResponseEntityExceptionHandler {
@ExceptionHandler(value
= { IllegalArgumentException.class, IllegalStateException.class })
protected ResponseEntity<Object> handleConflict(
RuntimeException ex, WebRequest request) {
String bodyOfResponse = "This should be application specific";
return handleExceptionInternal(ex, bodyOfResponse,
new HttpHeaders(), HttpStatus.CONFLICT, request);
}
}
- global error handling
- RESTful ResponseEntity response를 사용가능
Spring 5부터는
ResponseStatusException
@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) {
try {
Foo resourceById = RestPreconditions.checkFound(service.findOne(id));
eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response));
return resourceById;
}
catch (MyResourceNotFoundException exc) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "Foo Not Found", exc);
}
}
- custom exception class를 많이 만들 필요가 없다
- ControllerAdvice와 비교하면 통일된 에러 처리가 되지 않는다.
- 여러 컨트롤러에서 비슷한 코드가 반복될 수 있다.
- ControllerAdvice + ResponseStatusException 조합도 가능하다.
내가 사용하는 프로젝트에서는 이렇게 한다.
// properties 파일
spring:
mvc:
throw-exception-if-no-handler-found: true
web:
resources:
add-mappings: false
- url mapping에 실패하면 Exception이 나기 보다는 관련 에러 페이지를 찾아간다.
- 404 에러가 나면 BasicErrorController에서 처리를 하고 있는데, 이 값을 false로 주면 기본 핸들로 등록을 하지 않아 커스텀이 가능하다.
// ultron의 ErrorHandler.kt
@RestControllerAdvice
class ErrorHandler : ResponseEntityExceptionHandler() {
override fun handleNoHandlerFoundException(
ex: NoHandlerFoundException,
headers: HttpHeaders,
status: HttpStatus,
request: WebRequest
): ResponseEntity<Any> {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ErrorCd.RESOURCE_NOT_FOUND.getErrorResponse())
}
@ExceptionHandler(Exception::class)
fun handleMyException():ResponseEntity<Any>{
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(
Response(
status = "500",
error = Error(
code = -999,
message = "그냥 에러"
)
)
)
}
@ExceptionHandler(MyException::class)
fun handleMyException():ResponseEntity<Any>{
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(
Response(
status = "500",
error = Error(
code = -999,
message = "내가 그냥 만든 에러다 !"
)
)
)
}
}
반응형
'CS' 카테고리의 다른 글
[Spring] 스프링 Web MVC의 Dispatcher Servlet (0) | 2021.11.23 |
---|---|
웹 브러우저에서의 양방향 통신 방식 (0) | 2021.11.23 |
[Web] 세션과 쿠키란 무엇일까? (0) | 2021.10.04 |
도커와 쿠버네티스 (0) | 2021.08.31 |
함수형 프로그래밍 (0) | 2021.08.27 |