728x90
스프링 부트가 기본으로 제공하는 ExceptionResolver는 다음과 같다.
- ExceptionHandlerExceptionResolver
- ResponseStatusExceptionResolver
- DefaultHandlerExceptionResolver -> 우선순위가 가장 낮다.
ResponseStatusExceptionResolver
- HTTP 상태 코드를 지정해준다.
- 다음 두가지 경우를 처리해 준다
- @ResponseStatus 가 달려있는 예외
- ResponseStatusException 예외
BadRequestException
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad") //400 코드로 변경
public class BadRequestException extends RuntimeException{
//ResponseStatusResolver에서 잡아서 상태코드를 변경해 준다
//내부적으로 response.sendError(code, reason)으로 호출뒤 정상 호출을 반환한다.
}
ApiExceptionController
@GetMapping("/api/response-status-ex1")
public MemberDto responseStatusEx1(){
//상태 코드가 400으로 바뀌어져 있다
throw new BadRequestException();
}
- BadRequestException 예외가 컨트롤러 밖으로 넘어가면 ResponseStatusExceptionResolver 예외가 해당 애노테이션을 확인해서 오류 코드를 HttpStatus.BAD_REQUEST (400)으로 변경하고, 메시지도 담는다.
- ResponseStatusExceptionResolver 코드를 확인해보면 결국 response.sendError(statusCode, resolvedReason)를 호출하는 것을 확인할 수 있다.
ResponseStatusExceptionResolver

- 메시지 기능
- reason을 MessageSource에서 찾는 기능도 제공한다. reason = "error.bad"
messages.properties
error.bad= 잘못된 요청 오류입니다. 메시지 사용
- Messages.properties에 있는 값을 읽어서 메시지로 사용한다.
- @ResponseStatus는 개발자가 직접 변경할 수 없는 예외에는 적용할 수 없다. (애노테이션을 직접 넣어야 하는데, 내가 코드를 수정할 수 없는 라이브러리의 예외 코드 같은 곳에는 적용할 수 없다.)
- 추가로 애노테이션을 사용하기 때문에 조건에 따라 동적으로 변경하는 것도 어렵다. 이때는 ResponseStatusException 예외를 사용하면 된다.
ResponseStatusException
@GetMapping("/api/response-status-ex2")
public MemberDto responseStatusEx2(){
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
}
DefaultHandlerExceptionResolver
ApiExceptionController
@GetMapping("/api/default-handler-ex")
public String defaultException(@RequestParam Integer data){
//Spring 이 타입오류 를 400대 에러로 변경해 준다.
return "ok";
}
- DefaultHandlerExceptionResolver는 스프링 내부에서 발생하는 스프링 예외를 해결한다.
- 대표적으로 파라미터 바인딩 시점에 타입이 맞지 않으면 내부에서 TypeMismatchException 이 발생하는데, 이 경우 예외가 발생했기 때문에 그냥 두면 서블릿 컨테이너까지 오류가 올라가고, 결과적으로 500 오류가 발생한다
- 그런데 파라미터 바인딩은 대부분 클라이언트가 HTTP 요청 정보를 잘못 호출해서 발생하는 문제이다. HTTP에서는 이런 경우 HTTP 상태 코드 400을 사용하도록 되어 있다
- DefaultHandlerExceptionResolver는 이것을 500 오류가 아니라 HTTP 상태 코드 400 오류로 변경한다
DefaultHandlerExceptionResolver

ExceptionHandlerExceptionResolver
- API는 각 시스템 마다 응답의 모양도 다르고, 스펙도 모두 다르다.
- 예외 상황에 단순히 오류 화면을 보여주는 것이 아니라, 예외에 따라서 각각 다른 데이터를 출력해야 할 수도 있다. 그리고 같은 예외라고 해도 어떤 컨트롤러에서 발생했는가에 따라서 다른 예외 응답을 내려주어야 할 수 있다.
- 한마디로 매우 세밀한 제어가 필요하다. 앞서 이야기했지만, 예를 들어서 상품 API와 주문 API는 오류가 발생했을 때 응답의 모양이 완전히 다를 수 있다
- API 예외처리의 어려운 점
- HandlerExceptionResolver를 떠올려 보면 ModelAndView를 반환해야 했다. 이것은 API 응답에는 필요하지 않다.
- API 응답을 위해서 HttpServletResponse 에 직접 응답 데이터를 넣어주었다. 이것은 매우 불편하다. 스프링 컨트롤러에 비유하면 마치 과거 서블릿을 사용하던 시절로 돌아간 것 같다.
- 특정 컨트롤러에서만 발생하는 예외를 별도로 처리하기 어렵다. 예를 들어서 회원을 처리하는 컨트롤러에서 발생하는 RuntimeException 예외와 상품을 관리하는 컨트롤러에서 발생하는 동일한 RuntimeException 예외를 서로 다른 방식으로 처리하고 싶다면 어떻게 해야 할까?
@ExceptionHandler
- 스프링은 API 예외 처리 문제를 해결하기 위해 @ExceptionHandler라는 애노테이션을 사용하는 매우 편리한 예외 처리 기능을 제공하는데, 이것이 바로 ExceptionHandlerExceptionResolver이다. 스프링은 ExceptionHandlerExceptionResolver를 기본으로 제공하고, 기본으로 제공하는 ExceptionResolver 중에 우선순위도 가장 높다. 실무에서 API 예외 처리는 대부분 이 기능을 사용한다
ErrorResult
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
ApiExceptionV2 Controller
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@ResponseStatus(HttpStatus.BAD_REQUEST) //해당 어노테이션을 붙여서 상태코드를 원하는 값으로 반환해 줄수 있다
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalException(IllegalArgumentException e) {
//ExceptionHandlerExceptionResolver가 컨트롤러를 뒤져 해당 어노테이션이 있는지를 찾아서 해당 메소드를 호출해 준다.
log.error("[exceptionHandler] ex", e);
//해당 흐름을 찾아 정상 흐름으로 반환해 준다 -> 200 이 반환 된다.
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandler(UserException e) { //똑같을 경우 어노테이션 속성값 생략가능
log.error("[exceptionHandler] 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 exHandler(
Exception e) { //자식 에외를 모두 잡기 때문에 Exception을 잡게된다면 잡지않은 공통의 예외를 모두 잡아준다
log.error("[exceptionHandler] 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 애노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 된다. 해당 컨트롤러에서 예외가 발생하면 이 메서드가 호출된다. 참고로 지정한 예외 또는 그 예외의 자식 클래스는 모두 잡을 수 있다.
- 우선순위
- 스프링의 우선순위는 항상 자세한 것이 우선권을 가진다.
- @ExceptionHandler에 지정한 부모 클래스는 자식 클래스까지 처리할 수 있다. 따라서 자식예외 가 발생하면 부모예외처리() , 자식예외처리() 둘다 호출 대상이 된다.
- 그런데 둘 중 더 자세한 것이 우선권을 가지므로 자식예외처리() 가 호출된다. 물론 부모예외 가 호출되면 부모예외처리() 만 호출 대상이 되므로 부모예외처리() 가 호출된다
- 다양한 예외

- 예외 생략
- @ExceptionHandler 에 예외를 생략할 수 있다. 생략하면 메서드 파라미터의 예외가 지정된다
- 파리 미터와 응답
- @ExceptionHandler 에는 마치 스프링의 컨트롤러의 파라미터 응답처럼 다양한 파라미터와 응답을 지정할 수 있다
- https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-annexceptionhandler-args
Web on Servlet Stack
This part of the reference documentation covers support for Servlet stack, WebSocket messaging that includes raw WebSocket interactions, WebSocket emulation through SockJS, and publish-subscribe messaging through STOMP as a sub-protocol over WebSocket. 4.1
docs.spring.io
IllegalArgumentException 처리
- 컨트롤러를 호출한 결과 IllegalArgumentException 예외가 컨트롤러 밖으로 던져진다.
- 예외가 발생했어로 ExceptionResolver 가 작동한다. 가장 우선순위가 높은 ExceptionHandlerExceptionResolver 가 실행된다.
- ExceptionHandlerExceptionResolver는 해당 컨트롤러에 IllegalArgumentException을 처리할 수 있는 @ExceptionHandler 가 있는지 확인한다.
- illegalExHandle()를 실행한다. @RestController 이므로 illegalExHandle() 에도 @ResponseBody 가 적용된다. 따라서 HTTP 컨버터가 사용되고, 응답이 다음과 같은 JSON으로 반환된다.
- @ResponseStatus(HttpStatus.BAD_REQUEST)를 지정했으므로 HTTP 상태 코드 400으로 응답한다.
UserException 처리
- @ExceptionHandler에 예외를 지정하지 않으면 해당 메서드 파라미터 예외를 사용한다. 여기서는 UserException을 사용한다.
- ResponseEntity를 사용해서 HTTP 메시지 바디에 직접 응답한다. 물론 HTTP 컨버터가 사용된다.
- ResponseEntity 를 사용하면 HTTP 응답 코드를 프로그래밍해서 동적으로 변경할 수 있다. 앞서 살펴본 @ResponseStatus는 애노테이션이므로 HTTP 응답 코드를 동적으로 변경할 수 없다
Exception 처리
- throw new RuntimeException("잘못된 사용자") 이 코드가 실행되면서, 컨트롤러 밖으로 RuntimeException 이 던져진다.
- RuntimeException 은 Exception의 자식 클래스이다. 따라서 이 메서드가 호출된다.
- @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)로 HTTP 상태 코드를 500으로 응답한다
HTML 오류 화면
- ModelAndView를 사용해서 오류 화면(HTML)을 응답하는 데 사용할 수도 있다.
728x90
'스프링 MVC 2편(백엔드 웹 개발 활용 기술)' 카테고리의 다른 글
| Ch10. 스프링 타입 컨버터 소개 - 스프링 타입 컨버터 (0) | 2022.03.22 |
|---|---|
| Ch09. API 예외 처리 - @ControllerAdvice (0) | 2022.03.21 |
| Ch09. API 예외 처리 - HandlerExceptionResolver (0) | 2022.03.21 |
| Ch09. API 예외 처리 - 스프링 부트 기본 오류 처리 (0) | 2022.03.21 |
| Ch09. API 예외 처리 - API 예외 처리 시작 (0) | 2022.03.21 |