목차
서론
- 예외가 발생했을 때 HTML 페이지를 반환하는 경우 이외에 JSON으로 오류와 관련된 정보를 반환해야하는 경우가 있습니다.
- 오류 페이지의 경우 4xx, 5xx와 같은 오류 페이지를 단순히 고객에게 전달하면 끝이지만, API는 각 오류 상황에 맞는 오류 응답 스펙을 정의하고, 해당 데이터를 JSON으로 내려주어야 합니다.
- 지금부터 API의 경우 어떻게 예외를 처리하는지 알아보겠습니다.
- 먼저 서블릿이 어떻게 처리하는지 알아본 후, 스프링부트가 처리하는 방법에 대해 알아보겠습니다.
서블릿 - API 예외 처리
WebServerCustomizer - 에러 페이지 등록
@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory 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);
}
}
ApiExceptionController - API 예외 발생 컨트롤러
- 전달된 id가 ex일 경우, RuntimeException 발생
@Slf4j
@RestController
public class ApiExceptionController {
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if(id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
return new MemberDto(id, "hello " + id);
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
정상 호출
http://localhost:8080/api/members/spring
{
"memberId": "spring",
"name": "hello spring"
}
예외 발생 호출
http://localhost:8080/api/members/ex
<!DOCTYPE HTML>
<html>
<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>
<hr class="my-4">
</div> <!-- /container -->
</body>
</html>
문제점: JSON이 아닌 HTML 반환 → JSON을 반환하도록 수정
ErrorPageController - API 응답 추가
@RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> errorPage500Api(HttpServletRequest request, HttpServletResponse response) {
log.info("API errorPage 500");
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));
}
- produces = MediaType.APPLICATION_JSON_VALUE : 클라이언트가 요청하는 HTTP Header의 Accept의 값이 application/json이라면, 해당 메서드가 호출됩니다.
- 응답 데이터를 위해 Map을 만들고 status, message 키에 값을 할당했습니다. Jackson 라이브러리는 Map을 JSON 구조로 변환할 수 있습니다.
- ResponseEntity를 사용하여 응답하기 때문에, 메시지 컨버터가 동작하여 클라이언트에게 JSON이 반환됩니다.
실행
- http://localhost:8080/api/members/ex
- accept: application/json
{
"message": "잘못된 사용자",
"status": 500
}
스프링 부트 - BasicErrorController
스프링 부트의 경우, BasicErrorController가 발생한 예외를 처리합니다.
BasicErrorController
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {}
- errorHtml와 error 두 메서드를 확인할 수 있습니다.
- errorHtml() : produces = MediaType.TEXT_HTML_VALUE 이므로 클라이언트 요청의 Accept 헤더의 값이 text/html인 경우에 해당 메서드가 호출이 되고, view를 제공합니다.
- error() : 그 이외의 경우 호출이 됩니다. ResponseEntity로 HTTP Body에 JSON 데이터를 반환합니다.
옵션 설정
스프링 부트는 BasicErrorController가 제공하는 기본 기능들을 활용해 오류 API를 생성합니다.
다음 옵션들을 사용하여 더 자세한 오류 정보를 추가할 수 있습니다.
application.properties
erver.error.include-binding-errors=always
server.error.include-exception=true
server.error.include-message=always
server.error.include-stacktrace=always
- 물론, 오류 메시지는 보안상 위험할 수 있으므로 간결한 메시지만 노출하고, 로그를 통해 확인해야 합니다.
참고 스프링 부트의 BasicErrorController를 실행하기 위해선 WebServerCustomizer의 @Component를 주석처리해야 합니다.
실행
- GET
- http://localhost:8080/api/members/ex
- Accept: application/json
{
"timestamp": "2023-09-26T10:30:22.520+00:00",
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.RuntimeException",
"trace": "java.lang.RuntimeException: 잘못된 사용자\r\n\tat hello.exception.api.ApiExceptionController...,
"message": "잘못된 사용자",
"path": "/api/members/ex"
}
HandlerExceptionResolver
- BasicErrorController가 제공하는 오류 메시지를 수정하기 위해서 BasicErrorController를 확장해서 변경할 수 있습니다.
- 하지만 그것보다, @ExceptionHandler를 사용하는 것이 더 나은 방법입니다.
- 스프링 부트가 제공하는 BasicErrorController는 HTML 페이지를 제공하는 용도로 매우 편리하기 때문에 해당 용도로만 사용하고, API 오류 처리는 각각의 컨트롤러의 예외마다 다른 응답 결과를 출력해야 할 수 있을 만큼 매우 세밀하고 복잡하므로 @ExceptionHandler를 사용하는 것이 더 좋습니다.
- (ex. 회원과 관련된 API에서 발생한 예외와 상품과 관련된 API에서 발생한 예외 처리 방법이 다를 수 있습니다.)
지금부터 복잡한 API 오류는 어떻게 처리하는지 알아봅시다.
상태 코드 변환
- 예외가 발생해서 서블릿을 넘어 WAS까지 전달되면, HTTP 상태코드는 무조건 500으로 처리가 됩니다.
- 하지만, IllegalArgumentException의 경우 클라이언트에서 잘못된 매개변수를 전달하는 경우가 대다수이므로 이 경우엔 400으로 처리하고 싶습니다. 어떻게 해야 할까요?
ApiExceptionController - IllegalArugmentException 발생
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if(id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
if(id.equals("bad")) {
throw new IllegalArgumentException("잘못된 입력 값");
}
return new MemberDto(id, "hello " + id);
}
실행
http://localhost:8080/api/members/bad
{
"timestamp": "2023-09-26T10:39:49.426+00:00",
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.IllegalArgumentException",
"path": "/api/members/bad"
}
- 상태 코드가 500인 것을 확인할 수 있습니다.
ExceptionResolver
- 컨트롤러 밖으로 던져진 예외를 WAS에 도달하기 전에 가로채 동작 방식을 변경하여 전달합니다.
- 줄여서 ExceptionResolver라고 합니다.
ExceptionResolver 적용 전
컨트롤러에서 예외가 발생하면, 서블릿을 넘어 WAS에게 전달됩니다.
ExceptionResolver 적용 후
컨트롤러에서 발생한 예외를 서블릿이 ExceptionResolver를 호출하여 해결을 한 후 WAS에게 전달됩니다.
(ExceptionResolver로 예외를 해결해도, postHandle()은 호출되지 않습니다.)
HandlerExceptionResovler 인터페이스
public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}
- handler: 핸들러(컨트롤러)
- Exception ex: 핸들러(컨트롤러)에서 발생한 예외
MyHandlerExceptionResolver
@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");
//예외 먹음, sendError로 직접 정의한 예외 전달
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView();
}
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
}
반환값에 따른 동작 방식
return ModelAndView()
- ModelAndView를 반환하는 이유는 해당 예외는 정상 흐름처럼 변경하기 위해서입니다.
- 빈 ModelAndView() : 뷰를 렌더링하지 않고, 정상 흐름으로 서블릿이 리턴됩니다.
- ModelAndView 지정 : ModelAndView에 지정된 View, Model 의 정보를 통해 View를 렌더링합니다.
- null : 다음 ExceptionResolver를 찾아 실행합니다. 만약, 처리할 수 있는 ExceptionResolver가 없다면 예외 처리가 되지 않고 기존에 발생한 예외를 서블릿 밖인 WAS로 전달합니다.
ExceptionResolver 기능
- 예외 상태 코드를 변환해서 반환합니다.
- response.sendError(xxx)를 호출하고 현재 예외는 정상으로 처리하여 상태 코드에 따른 오류를 처리하도록 위임합니다.
- 이후 WAS는 서블릿 오류 페이지를 찾아 내부 호출합니다. (ex) /error 호출)
- 뷰 템플릿을 처리합니다.
- ModelAndView에 값을 채워 반환하면, 새로운 오류 화면 뷰를 렌더링하여 제공합니다.
- API 응답을 처리합니다
- response.getWriter().println("hello"); 와 같이 HTTP 응답 바디에 직접 데이터를 넣어 반환할 수 있습니다. JSON을 담으면 API 응답 처리를 할 수 있습니다
WebConfig - ExceptionResolver 등록
/**
* 기본 설정을 유지하면서 추가
*/
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
참고 extendHandlerExceptionResolver()가 아닌 configureHandlerExceptionResolvers()를 사용하면 스프링이 기본으로 등록하여 사용하던 ExceptionResolver가 제거되므로 주의해야 합니다.
실행
- http://localhost:8080/api/members/ex → HTTP 상태 코드 500
- http://localhost:8080/api/members/bad → HTTP 상태 코드 400 (MyExceptionResolver 동작)
HandlerExceptionResolver - 예외 마무리하기
- 현재 작성한 코드는 sendError를 통해 WAS까지 예외를 던지고, WAS가 다시 오류 페이지 정보를 찾기 위해 /error를 호출합니다.
- 번거로운 해당 과정없이 ExceptionHandler에서 예외 처리를 마무리할 수 있습니다.
UserException - 사용자 정의 예외 생성
예시를 위해 사용자 정의 예외를 생성합니다.
package hello.exception.exception;
public class UserException extends RuntimeException {
public UserException() {
super();
}
public UserException(String message) {
super(message);
}
public UserException(String message, Throwable cause) {
super(message, cause);
}
public UserException(Throwable cause) {
super(cause);
}
protected UserException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
ApiExceptionController - UserException 발생
@Slf4j
@RestController
public class ApiExceptionController {
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if(id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
if(id.equals("bad")) {
throw new IllegalArgumentException("잘못된 입력 값");
}
if(id.equals("user-ex")) {
throw new UserException("사용자 오류");
}
return new MemberDto(id, "hello " + id);
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
실행
http://localhost:8080/api/members/user-ex → 500 에러 (BasicErrorController)
UserHandlerExceptionResolver - UserException 처리
@Slf4j
public class UserHandlerExceptionResolver implements HandlerExceptionResolver {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
if(ex instanceof UserException) {
log.info("UserException resolver to 400");
//http header에 따른 두 가지 처리 (html or application/json)
String acceptHeader = request.getHeader("accept");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST); //400
if("application/json".equals(acceptHeader)) {
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("ex", ex.getClass());
errorResult.put("message", ex.getMessage());
//객체를 json으로 변경
String result = objectMapper.writeValueAsString(errorResult);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(result);
return new ModelAndView();
} else {
//TEXT/HTML
return new ModelAndView("error/500");
}
}
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
}
WebConfig - UserHandlerExceptionResolver 등록
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
resolvers.add(new UserHandlerExceptionResolver());
}
실행
http://localhost:8080/api/memebers/user-ex
Accept: application/json
{
"ex": "hello.exception.exception.UserException",
"message": "사용자 오류"
}
Accept: text/html
<!DOCTYPE HTML>
<html>
...
</html>
정리
- sendError가 아닌 해당 ExceptionResolver에서 예외를 처리합니다.
- 따라서, 예외가 발생해도 WAS까지 예외가 전달이 되지 않고 스프링 MVC에서 처리가 끝나므로 WAS 입장에서는 정상 처리된것으로 인식합니다.
- 이렇게 예외를 ExceptionResolver에서 모두 처리한다는 것이 핵심입니다.
- 하지만, 매번 ExceptionResolver를 구현하기엔 번거롭고 복잡하므로 스프링은 ExceptionResolver를 제공합니다.
스프링이 제공하는 ExceptionResolver
스프링 부트가 기본으로 제공하는 ExceptionResolver의 종류는 다음과 같이 3가지입니다.
(HandlerExcpetionResolverComposite에 다음 순서와 우선순위로 등록합니다.)
- ExceptionHandlerExceptionResolver (가장 중요)
- ResponseStatusExceptionResolver
- DefaultHandlerExceptionResolver → 우선 순위가 가장 낮다.
ResponseStatusExceptionResolver
- 해당 ExceptionResolver는 예외의 HTTP 상태 코드를 변경해줍니다.
- 다음 두 가지 경우를 처리합니다.
- @ResponseStatus가 붙어있는 예외
- ResponseStatusException 예외
1. @ResponseStatus
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}
ApiController - BadRequestException 발생
@GetMapping("/api/response-status-ex1")
public String responseStatusEx1() {
throw new BadRequestException();
}
실행
http://localhost:8080/api/response-status-ex1?message=
{
"timestamp": "2023-09-27T12:23:00.043+00:00",
"status": 400,
"error": "Bad Request",
"exception": "hello.exception.exception.BadRequestException",
"message": "잘못된 요청 오류",
"path": "/api/response-status-ex1"
}
동작 방식
- BadRequestException 예외가 발생하여 컨트롤러 밖으로 전달되면, ResponseStatusExceptionResolver가 해당 애노테이션을 확인하여 오류 코드를 HttpStatus.BAD_REQUEST(400) 으로 변경하고, reason에 해당하는 값도 메시지에 담습니다.
- ResponseStatusExceptionResolver 코드를 확인해보면, response.sendError(statusCode, resolvedReason)를 호출하는 것을 확인할 수 있습니다.
- sendError(400)을 호출했기 때문에 WAS에서 다시 오류 페이지 (/error)를 내부 요청합니다.(accept 헤더가 application/json이라면 json을 리턴합니다.)
부가 기능: 메시지 코드화
reason을 MessageSource에서 찾는 기능을 제공합니다.
//@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad")
public class BadRequestException extends RuntimeException{
}
resources/messages.properties
error.bad=잘못된 요청 오류입니다. 메시지 사용
실행
{
"timestamp": "2023-09-27T12:31:39.803+00:00",
"status": 400,
"error": "Bad Request",
"exception": "hello.exception.exception.BadRequestException",
"message": "잘못된 요청 오류입니다. 메시지 사용",
"path": "/api/response-status-ex1"
}
문제점
- @ResponseStatus는 개발자가 직접 정의한 예외에만 넣을 수 있습니다. 즉, 개발자가 수정할 수 없는 라이브러리 예외 코드에는 적용할 수 없습니다.
- 이 때, ResponseStatusException 을 사용하면 됩니다.
2. ResponseStatusException
ApiExceptionController
@GetMapping("/api/resopnse-status-ex2")
public String responseStatusEx2() {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
}
실행
http://localhost:8080/api/response-status-ex2
{
"timestamp": "2023-09-27T12:41:30.061+00:00",
"status": 404,
"error": "Not Found",
"exception": "org.springframework.web.server.ResponseStatusException",
"message": "잘못된 요청 오류입니다. 메시지 사용",
"path": "/api/response-status-ex2"
}
DefaultHandlerExceptionResolver
- DeafultHandlerExceptionResolver는 스프링 내부에서 발생한 예외를 일부 해결합니다.
- 예를 들어 바인딩 타입이 맞지 않을 때 발생하는 TypeMismatchException의 경우, WAS로 예외가 전달되면 500오류가 발생합니다.
- 하지만 파라미터 바인딩은 대부분 클라이언트가 HTTP 요청 호출을 잘못하여 발생하는 경우가 많으므로, HTTP 스펙에 따르면 상태 코드가 400이 더 적절합니다.
- DefaultHandlerExceptionResolver는 이런 경우 500이 아니라 400오류 코드로 변경합니다.
- 이외에도 DefaultHandlerExceptionResolver에는 스프링 내부 오류를 어떻게 처리할지 많은 내용이 정의되어 있습니다.
내부 코드
response.sendError(HttpServletResponse.SC_BAD_REQUEST) (400)
- 결국, response.sendError()를 통해 문제를 해결합니다.
- sendError(400)을 호출했기 때문에, WAS에서 다시 오류 페이지(/error)를 요청합니다.
ApiController - TypeMismatchException 발생
@GetMapping("/api/default-handler-ex")
public String defaultException(@RequestParam Integer data) {
return "ok";
}
실행
- Integer 타입인 data에 String 타입인 "hello" 입력
- http://localhost:8080/api/default-handler-ex?data=hello&message=
{
"timestamp": "2023-09-27T12:59:09.678+00:00",
"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: \"hello\"",
"path": "/api/default-handler-ex" }
ExceptionHandlerExceptionResolver (가장 중요)
직접 정의한 MyExceptionResolver의 문제점
- ModelAndView 반환
- API 응답에 필요하지 않은 ModelAndView를 생성해 반환해야 합니다.
- HttpServeltResponse에 응답값 직접 입력
- 마치 과거 서블릿을 사용하면 시절처럼 직접 Map을 생성하고, objectMapper를 통해 json으로 변환하는 등 번거로운 작업을 거쳤습니다.
- 특정 컨트롤러에서 발생한 예외 별도 처리 불가
- 예를 들어 회원 컨트롤러에서 발생한 RuntimeException과 상품 컨트롤러에서 발생한 RuntimeException을 다르게 처리하고 싶을 경우 방법이 없습니다.
해결책: @ExceptionHandler
- 스프링 부트가 제공하는 @ExceptionHandler를 통해 위의 문제를 해결할 수 있습니다.
- 이것이 ExceptionHandlerExceptionResolver입니다.
- 스프링 부트가 기본으로 제공하는 ExceptoinResolver 중 우선 순위가 가장 높습니다.
- 실무에서 API 예외 처리는 대부분 이 기능을 사용합니다.
ErrorResult - API 응답 객체 생성
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
ApiExceptionV2Controller
@Slf4j
@RestController
public class ApiExceptionV2Controller {
/**
* 예외 처리
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
log.error("[exceptionHandle] ex", e);
ErrorResult errorResult = new ErrorResult("USER_EX", e.getMessage());
return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandle(Exception e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("EX", "내부 오류");
}
/**
* 예외 호출
*/
@GetMapping("/api2/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if(id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
if(id.equals("bad")) {
throw new IllegalArgumentException("잘못된 입력 값");
}
if(id.equals("user-ex")) {
throw new UserException("사용자 오류");
}
return new MemberDto(id, "hello " + id);
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
@ExceptionHandler
- 해당 컨트롤러에서 처리할 예외 타입을 지정합니다. (해당 컨트롤러에서 벗어나 발생한 예외는 처리하지 않습니다.)
- 해당 메서드 매개변수에 지정한 예외 타입와 같을 경우 생략할 수 있습니다.
- 지정한 예외와 자식 클래스 모두 처리할 수 있습니다.
우선순위
스프링의 우선순위는 항상 자세한 것이 우선권을 갖습니다.
@ExceptionHandler(부모예외.class)
public String 부모예외처리()(부모예외 e) {}
@ExceptionHandler(자식예외.class)
public String 자식예외처리()(자식예외 e) {}
- 자식 예외가 호출된다면, 부모 예외 처리(), 자식 예외 처리() 둘 다 호출 대상이 되지만, 더 자세한 자식 예외 처리()가 호출됩니다.
- 부모 예외가 호출되면, 당연히 부모 예외 처리()만 호출 대상이 되므로 부모 예외 처리()가 호출됩니다.
다양한 예외
다음과 같이 다양한 예외를 한 번에 처리할 수 있습니다.
@ExceptionHandler({AException.class, BException.class})
public String ex(Exception e) {
log.info("exception e", e);
}
참고 파라미터와 응답
@ExceptionHandler에는 마치 스프링의 컨트롤러처럼 다양한 파라미터와 응답을 지정할 수 있습니다.
공식 메뉴얼 - https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-annexceptionhandler-args
실행
http://localhost:8080/api2/members/bad
400Bad Request
{
"code": "BAD",
"message": "잘못된 입력 값"
}
실행 흐름
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
- 컨트롤러에서 IllegalArgumentException 예외가 발생합니다.
- 서블릿에게 전달이 되고 ExceptionResolver가 동작합니다. 그 중, 가장 우선 순위가 높은 ExceptionHandlerExceptionResolver가 실행됩니다.
- ExceptoinHandlerExceptionResolver는 해당 컨트롤러에 IllegalArgumentException을 처리할 수 있는 @ExceptionHandler가 있는지 확인합니다.
- 있다면, illegalHandle()를 실행합니다. 해당 메서드는 @RestController이므로 @ResponseBody가 적용되어 HTTP 컨버터가 사용이 되므로 JSON을 반환합니다.
- 또한, @ResponseStatus(HttpStatus.BAD_REQUEST)를 지정했으므로, HTTP 상태 코드 400을 응답합니다.
기타 - HTML 오류 화면
다음과 같이 ModelAndView를 사용해 오류 화면(HTML)을 응답하는데 사용할 수도 있습니다.
@ExceptionHandler(ViewException.class)
public ModelAndView ex(ViewException e) {
log.info("exceptoin e", e);
return new ModelAndView("error")
}
@ControllerAdvice
- @ExceptionHandler를 통해 예외를 깔끔하게 처리할 수 있게 되었지만, 한 가지 문제점이 있습니다.
- 정상 코드와 예외 처리 코드가 하나의 컨트롤러에 섞여 있습니다.
- 이를 @ControllerAdvice 또는 @RestControllerAdvice를 사용하여 해결할 수 있습니다.
- @RestControllerAdvice는 @RestController + @ControllerAdvice 입니다.
ExControllerAdvice - @RestControllerAdvice
@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
log.error("[exceptionHandle] ex", e);
ErrorResult errorResult = new ErrorResult("USER_EX", e.getMessage());
return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandle(Exception e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("EX", "내부 오류");
}
}
- @ControllerAdvice는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여하는 역할을 합니다.
- @ControllerAdvice에 대상을 지정하지 않으면 모든 컨트롤러에 적용됩니다. (글로벌 적용)
- @RestControllerAdvice는 @RestController + @ControllerAdvice입니다.
대상 컨트롤러 지정 방법
- 특정 애노테이션, 특정 패키지, 특정 컨트롤러에 지정할 수 있습니다.
- 패키지 지정의 경우, 해당 패키지와 그 하위에 있는 컨트롤러가 대상이 됩니다. (보통 사용하는 방식입니다.)
- 스프링 공식 문서 - https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-anncontroller-advice
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
결론
@ExceptionHandler와 @ControllerAdvice 조합을 사용하여 예외를 깔끔하게 해결할 수 있습니다.
출처
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
'Backend > Spring | SpringBoot' 카테고리의 다른 글
[SpringBoot] 파일 업로드 (2) | 2023.10.02 |
---|---|
[SpringBoot] 예외 처리 - 오류 페이지 (0) | 2023.09.26 |
[SpringBoot] 필터와 인터셉터 (ArgumentResolver + @Login 활용) (0) | 2023.09.21 |
[SpringBoot] 쿠키와 세션(HttpSession) (0) | 2023.09.19 |
[SpringBoot] HTTP 메시지 컨버터 (+ArugumentResolver, ReturnValueHandler) (0) | 2023.09.01 |