CS

[Spring] 스프링의 에러 처리(Error Handling)

뽀글보리 2021. 10. 29. 20:10
반응형

스프링 부트 프로젝트를 하나 띄워서 없는 주소로 요청하면 다음과 같은 예외가 발생한다.

이 응답은 어디서 온걸까?

서블릿의 예외처리 과정

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에는 아무것도 하지 않는다는 단점

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