예외 발생 방법

우선 예외를 발생시키는 방법에 대해 알아야 예외 처리를 할 수 있습니다.

Exception 클래스 선언

throw new RuntimeException("예외 발생!");

Exception 클래스들을 새로 선언하여 에러를 발생시킬 수 있습니다. Exception의 하위 클래스들은 다양하게 존재합니다. image

response.sendError()

@GetMapping("/error-404")
public void error404(HttpServletResponse response) throws IOException {
	response.sendError(404, "404 오류!");
}

위와 같이 원하는 코드로 에러를 발생시키려면 HttpServletResponse의 sendError()함수를 사용하면 됩니다.

[예외 페이지 제공]

HTML등과 같이 웹 페이지에서 에러가 났을 때 사용자에게 잘못된 페이지라는 정보를 전달하는 페이지가 필요합니다.
아래는 예외 처리 방법 중 예외 페이지를 처리하는 방법에 대해 설명하겠습니다.

사전 작업

application.properties파일에서 아래 줄을 추가하여 기본적으로 제공되는 예외 페이지를 꺼주세요.

 server.error.whitelabel.enabled=false

1. Servlet에 오류 페이지 등록(WebServerFactoryCustomizer)

오류의 종류에 따라 서블릿에서 내 서버의 특정 경로로 이동할 수 있게끔 할 수 있도록 하는 기능이 있습니다.
바로 WebServerFactoryCustomizer라는 Interface를 구현함으로써 가능합니다.

@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {  
    @Override  
    public void customize(ConfigurableWebServerFactory factory) {  
        ErrorPage errorPage404 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");  
        factory.addErrorPages(errorPage500);  
    }  
}

ErrorPage라는 클래스를 만들고, 이걸 factory에 추가하면 등록했던 오류 상황에 맞는 경로로 오류가 발생했을 때 이동하게 됩니다.
예를들어 NOT_FOUND(404)에러가 발생되면 자동으로 /error-page/404 경로를 실행하게 됩니다. 그럼 error-page/404는 404 에러 상황에 보여줄 에러 페이지를 제공하는 기능을 Controller를 통해 구현하면 됩니다.

@Controller
public class ErrorPageController {
	
	@RequestMapping("/error-page/500")  
	public String errorPage500() {  
	    return "error-page/500";  
	}
}

그럼 결과적으로 resources/templates/error-page/404.html을 보여주게 됩니다.

예외 정보 확인

예외 코드, 메시지 등 예외에 대한 정보를 HttpServletRequest의 Attribute를 통해 확인할 수 있습니다.

  • ERROR_EXCEPTION: 예외
  • ERROR_EXCEPTION_TYPE: 예외 타입
  • ERROR_MESSAGE: 오류 메시지
  • ERROR_REQUEST_URI: 클라이언트 요청 URI
  • ERROR_SERVLET_NAME: 오류가 발생한 서블릿 이름
  • ERROR_STATUS_CODE: HTTP 상태 코드

위와 같은 정보들을 request.getAttribute()에 넣어주면 예외의 정보들을 알 수 있습니다.

2. ⭐️ 스프링 부트 기본 예외 페이지 기능(BasicErrorController)

사전 작업

위 WebServerFactoryCustomizer기능이 사용되어 있으면 안되기 때문에, 해당 기능이 Bean에 등록되지 않도록 WebServerCustomizer클래스의 @Component를 주석 처리해줍시다.

스프링 부트에선 기본적으로 BasicErrorController라는 클래스에서 특정 경로에 있는 html 파일을 예외 페이지로 보여주도록 설정되어 있습니다.

뷰템플릿

  • resources/templates/error/500.html
  • resources/templates/error/5xx.html

정적 리소스( static , public )

  • resources/static/error/400.html
  • resources/static/error/404.html
  • resources/static/error/4xx.html

적용 대상이 없을 때 뷰 이름(error )

  • resources/templates/error.html

파일 이름이 해당 코드의 예외 페이지를 담당하고 4xx같은 경우는 400번대의 모든 오류를 처리해줍니다.

예외 정보 확인

기본적으로 BasicErrorController는 model에 예외와 관련된 정보들을 담아서 뷰에 전달합니다.
이 값들을 활용하면 예외에 관련된 정보를 출력할 수 있습니다.

예시

<!DOCTYPE HTML>  
<html xmlns:th="http://www.thymeleaf.org">  
<head>  
    <meta charset="utf-8">  
</head>  
<body>  
<div class="container" style="max-width: 600px">  
    <div class="py-5 text-center">  
        <h2>500 오류 화면 스프링 부트 제공</h2> 
    </div>  
    <div>        
	    <p>오류 화면 입니다.</p>  
    </div>  
    <ul>        
	    <li>오류 정보</li>  
        <ul>            
	        <li th:text="|timestamp: ${timestamp}|"></li>  
            <li th:text="|path: ${path}|"></li>  
            <li th:text="|status: ${status}|"></li>  
            <li th:text="|message: ${message}|"></li>  
            <li th:text="|error: ${error}|"></li>  
            <li th:text="|exception: ${exception}|"></li>  
            <li th:text="|errors: ${errors}|"></li>  
            <li th:text="|trace: ${trace}|"></li>  
        </ul> 
    </ul>
    <hr class="my-4">  
</div> <!-- /container -->  
</body>  
</html>

[API 예외 제공]

예외가 발생했을 때 HTML로 웹 페이지를 제공하는것 외에도 API로 예외가 발생하였을 때 JSON형태로 예외 상태를 전달할 수 있어야 합니다.
이럴 때 아래와 같은 방법을 사용하여 예외 메시지를 제공합니다.

1. @RequestMapping의 produces 설정

사전 작업

위 WebServerFactoryCustomizer기능이 다시 사용되어야 하기 때문에 WebServerCustomizer@Component주석을 다시 풀어주셔야 합니다.

@RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> errorPage500Api(HttpServletRequest request, HttpServletResponse response) {
	
     Map<String, Object> result = new HashMap<>();
     Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
     result.put("status", request.getAttribute(ERROR_STATUS_CODE));
     result.put("message", ex.getMessage());
	
     Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
	
     return new ResponseEntity(result, HttpStatus.valueOf(statusCode));
 }

Controller에 RequestMapping을 추가하여 에러 화면을 보여주는 부분에서 조금 더 나아가 produces 옵션을 MediaType.APPLICATION_JSON_VALUE으로 지정하였습니다.
이렇게 하면 /error-page/500경로를 Header의 Accept가 application/json으로 설정되어서 요청했을 때는 해당 함수가 실행되게 됩니다.

2. 스프링 부트 기본 오류 처리 기능(BasicErrorController)

사전 작업

위 WebServerFactoryCustomizer기능이 사용되어 있으면 안되기 때문에, 해당 기능이 Bean에 등록되지 않도록 WebServerCustomizer클래스의 @Component를 주석 처리해줍시다.

WebServerFactoryCustomizer를 따로 설정하지 않으면 스프링 부트가 기본적으로 에러 데이터를 전달합니다.

예시

{  
	"timestamp": "2021-04-28T00:00:00.000+00:00", 
	"status": 500,  
	"error": "Internal Server Error",  
	"exception": "java.lang.RuntimeException",  
	"trace": "java.lang.RuntimeException: 잘못된 사용자...",
	"message": "잘못된 사용자",
	"path": "/api/members/ex"
}

더 자세한 오류 정보

application.properties에 해당 옵션을 설정하면 더 자세한 오류 정보를 추가할 수 있습니다.

  • server.error.include-binding-errors=always
  • server.error.include-exception=true
  • server.error.include-message=always
  • server.error.include-stacktrace=always

always는 무조건 표시, on_param은 파라미터를 추가해야지만, false는 표시 안하는 설정입니다.
보안상 위험할 수 있기 때문에 false로 하고 로그를 통해서 확인하는것을 권장합니다.

3. HandlerExceptionResolver 사용하기

기본적으로 서버에서 오류가 발생하면 500상태코드의 에러가 발생하게 됩니다.
그러나 사용자와 관련된 에러는 400 상태코드를 반환하는 등 여러 상황에 따라 알맞은 400번대의 상태코드로 반환해야하는 상황이 있습니다.

이럴 때 HandlerExceptionResolver를 사용하면 에러의 종류별로 상태 코드, 반환 형태 등 여러가지를 설정할 수 있습니다.
자세한 내용은 HandlerExceptionResolver를 확인해 주세요.

4. 스프링이 제공하는 ExceptionResolver 사용하기

스프링이 기본적으로 제공하는 ExceptionResolver는 3종류가 있습니다.

1) ExceptionHandlerExceptionResolver

@ExceptionHandler를 처리하는 resolver입니다.
@ExceptionHandler가 많이 쓰이는 만큼 API 예외 처리는 대부분 이 resolver를 통해 해결됩니다.

@ExceptionHandler는 아래에서 배우기 때문에 우선은 넘어가고, 아래에서 자세히 배우시면 됩니다.

2) ResponseStatusExceptionResolver

ResponseStatusExceptionResolver 페이지를 확인해주세요.

3) DefaultHandlerExceptionResolver

DefaultHandlerExceptionResolver는 스프링 내부에서 발생하는 에러를 해결합니다.

예를 들어 숫자가 들어와야 하는 파라미터에 타입이 맞지 않는 문자열 등이 들어오게 되면 스프링 내부에서 에러가 발생하고, 이 상황은 사용자가 파라미터값을 잘못 넣는 것이니 500번 에러를 발생시키는 것이 아닌 response.sendError(HttpServletResponse.SC_BAD_REQUEST)를 통해 400번 에러를 자동으로 발생시키는 기능을 합니다.

예시

@GetMapping("/api")
public String defaultException(@RequestParam Integer data) {
     return "ok";
}

위와 같은 컨트롤러의 함수에 http://localhost:8080/api?data=abc와 같이 data 파라미터를 문자열로 넣게 되면 DefaultHandlerExceptionResolver가 자동으로 발생하여 아래와 같은 결과를 반환합니다.

{
     "status": 400,
     "error": "Bad Request",
     "exception": "org.springframework.web.method.annotation.MethodArgumentTypeMismatchException",
     "message": "Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'; nested exception is java.lang.NumberFormatException: For input string: \"abc\"",
     "path": "/api"
 }
 

5. ⭐️ @ExceptionHandler 활용

API의 예외 처리는 API의 종류에 따라서 다르게 형태를 구성하여 제공해야 할 경우가 많지만, HandlerExceptionResolver를 직접 구현하면서 처리하기에는 많이 복잡합니다.
그럴 때는 @ExceptionHandler을 사용하여 예외 처리를 하면 됩니다.

자세한 내용은 @ExceptionHandler을 확인해 주세요.