-
Spring MVC 2 - API 예외 처리 @ExceptionResolverWeb/Spring 2023. 11. 22. 00:39
- 스프링 부트가 기본으로 제공하는 ExceptionResolver는 다음과 같다.
1. ExceptionHandlerExceptionResolver : @ExceptionHandler 처리 (API 예외 처리 대부분 이 기능 사용)
2. ResponseStatusExceptionResolver : HTTP 상태 코드 지정해줌 (@ResponseStatus)
3. DefaultHandlerExceptionResolver : 스프링 내부 기본 예외를 처리
* HandlerExceptionResolverComposite 에 위 순서로 등록됨 -> null반환하면 계속 다음 것 찾음 -> 없으면 걍 500일 듯(WAS)
1. ReponseStatusExceptionResolver
- 예외에 따라 HTTP 상태 코드를 지정해주는 역할을 수행 (HandlerExceptionResolver에서 response.sendError 편의 기능)
- 두 가지 경우 위 Resolver가 호출되어 처리 : 1. @ResponseStatus가 달려있는 예외 , 2 ResponseStatusException 예외
(사용자 정의 예외에 적용 어노테이션)
1.1 @ResponseStatus
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류") public class BadRequestException extends RuntimeException{ }
- 위 Exception이 컨트롤러 밖으로 나가면, ResponseStatusExceptionResolver가
던저진 예외의 어노테이션 확인 -> 오류 코드 변경, 메시지 담기 해준다.
- 결국 response.sendError()해주는 것과 같다. ( HandlerExceptionResolver에서도 해봤음)
*reason은 MessageSource 기능도 제공한다.
1.2 ResponseStatusException
- @ResponseStatus는 사용자가 변경 불가능한 예외에 대해서는 적용이 안된다.
- 예외에 위 어노테이션을 넣어야하는데.. 라이브러리 코드 등에서는 적용하기 힘듦)
- 또한 동적 변경도 어렵다
- 이럴때 ResponseStatusException을 사용하자
@GetMapping("/api/response-status-ex2") public String responseStatusEx2() { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException()); }
- 에러가 발생하면 한번 wrapping해서 다시 던지는 방식
2. API 예외 처리 - 스프링 기본 제공 (DefaultHandlerExceptionResolver)
- DefaultHandlerExceptionResolver는 스프링 내부에서 발생하는 스프링 예외를 해결한다.
- 대표적으로 파라미터 바인딩 시점에 타입 오류 (TypeMismatchException)
(ArgumentResolver -> TypeConverter 시점에서 발생하는 오류 등)
- 원래는 예외 발생 500 Error이지만, 바인딩 오류는 클라이언트 측 오류임 -> 400으로 바꿔준다.
- DefaultHandlerExceptionResolver.handleTypeMismatch를 보면 -> response.sendError()하는 것을 볼 수 있다.
* 기본으로 제공되는 2가지는 상태코드 변경하는 정도의 기능만 제공해주었다.
다른 처리를 위해 HandlerExceptionResolver를 사용할 수도 있지만, 매우 복잡하다.
스프링은 이 문제를 해결하기 위해 @ExceptionHandler를 제공해준다.
3. API 예외 처리 - @ExceptionHandler + ExceptionHandlerExceptionResolver
- HTML 화면 오류
웹 브라우저에 HTML 화면을 제공하고 싶으면, BasicErrorController를 사용
- API 오류
ExceptionHandlerExceptionResolver를 활용
-> 기존의 BasicErrorController + HandlerExceptionResolver 조합 : ModelAndView를 반환해야함 -> API 응답에 필요x
response를 직접 사용 -> 매우 불편 + 거의 서블릿
-> 또한 특정 컨트롤러에서만 발생하는 예외를 별도로 처리하기 어렵다. (위에 제공된 건 둘자 글로벌함)
*HandlerExceptionResolver에서 예외를 모두 잡기 (헤더확인 ->API 바로응답, HTML -> ModelAndView 활용)
-> 일단 예외가 발생하면 작동
*BasicErrorController (헤더가 Application/json (HTML이 아니면) 기본 json에러 만들어서 줌 -> ExceptionResolver에서 예외 상태만 변경한 경우 사용됨)
-> 예외 어디서 안잡거나, ExceptionResolver에서 response.sendError만 한 경우 Basic작동
3.1 ExceptionHandler
- 위에 살펴본 두가지 ExceptionResolver의 한계를 @ExceptionHandler로 해결가능하다.
- 스프링은 ExceptionHandlerExceptionResolver를 기본으로 제공 우선순위도 높고 대부분 이걸로 처리 가능
@Data @AllArgsConstructor public class ErrorResult { private String code; private String message; }
@Slf4j @RestController public class ApiExceptionV2Controller{ @RequestStatus(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){ 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); } }
- @ExceptionHandler 어노테이션 선언 -> 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주자
(컨트롤러 예외 발생시 일단 @ExceptionHandler가 있다면 해결을 시도한다) + 지정 예외 자식 까지 포함
- 예외 우선순위는 항상 자세한 것이 우선순위가 있다.
- @ExceptionHandler가 작동하면 예외를 처리했음으로 정상 흐름으로 진행된다 (상태코드 지정 x시 200임)
- 마치 Exception으로 매핑된 컨트롤러를 다시 호출하는 것으로 생각할 수 있다.
> 다양한 예외 : @ExceptionHandler({})배열로 한번에 처리할 수 있다.
> 예외 생략 : 파라미터에 정의된 예외를 자동으로 추정해준다.
> 파라미터 : @ExceptionHandler는 마치 스프링 컨트롤러 처럼 다양한 파라미터를 지정할 수 있다 (컨트롤러보단 적음)
*공식메뉴얼 참고
*일반적인 view 반환도 가능함 -> @RestController가 아닐 시 , 또한 ModelAndView를 반환할 수 있다.
String 반환하면 view로 생각하고 처리해줌 물론 ResponseBody를 적어두면 다시 body에 적음
* ResponseEntity를 사용하면 HTTP 응답 코드를 동적으로 변경할 수 있다. (어노테이션은 동적으로 처리 불가)
4. API 예외 처리 - @ControllerAdvice
- @ExceptionHandler로 예외를 깔끔하게 처리할 수 있다. 근데 정상 코드와 예외 코드가 하나에 컨트롤러에 섞여 있다.
- @ControllerAdvice, @RestControllerAdvice를 사용하면 이를 분리할 수 있다.
@RestControllerAdvice pulbic 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기능을 부여해줌
- 대상을 지정하지 않으면, 모든 컨트롤러에 적용된다.
- @RestControllerAdvice는 @ControllerAdvice와 같고, @RespnseBody가 추가되어 있다.
4.1 타겟 지정 방법
// Target all Controllers annotated with @RestController @ControllerAdvice(annotations = RestController.class) // Target all Controllers within specific pakages @ControllerAdvice("org.example.controllers") //Target all Controllers assinable to specific classes @ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
- 자세한 사항은 공식문서 확인하자
'Web > Spring' 카테고리의 다른 글
Spring - @PathVariable 기본값 (0) 2024.08.11 스프링 MVC - 외부 리소스 경로 읽어오기 (0) 2024.08.11 Spring MVC 2 - API 예외 처리 HandlerExceptionResolver (0) 2023.11.22 Spring MVC (2) - 예외 처리와 오류 페이지 (0) 2023.11.21 Spring MVC (2) - 스프링 포맷터 (0) 2023.11.21