오류 페이지는 단순히 고객에게 오류 화면을 보여주고 끝이지만, API 예외 처리는 각 오류 상황에 맞는 오류 응답 스펙을 자율적으로 정하고, JSON으로 데이터를 내려주어야 함
ErrorPageController.java
@RequestMapping("/error-page/500")
...
}
@RequestMapping(value = "/error-page/500",
produces = MediaType.APPLICATION_JSON_VALUE)
// 위의 컨트롤러와 같은 URL을 처리하는 컨트롤러여도 produces에 따라
// 클라이언트의 accept 타입이 application/json인 경우에는 이 컨트롤러가 호출됨
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));
}
@RequestMapping
도 자세한게 더 높은 우선순위를 가짐BasicErrorController
주의
BasicErrorController
는 HTML 페이지를 제공하는 경우에는 매우 편리, 하지만 API 오류 처리는 매우 세밀하고 복잡BasicErrorController
은 HTML 화면을 처리할 때 사용하고, API는 오류 처리는 @ExceptionHandler
를 사용하는것이 좋음IllegalArgumentException
을 처리하지 못해서 컨트롤러 밖으로 넘어가는 일이 발생하면 상태코드 500으로 처리되는데, HTTP 상태코드를 400으로 처리하고싶다면 ?
컨트롤러 밖으로 던져진 예외를 해결하고, 동작 방식을 변경하고 싶으면 HandlerExceptionResolver
를 사용
ExceptionResolver
적용 전에는 컨트롤러에서 예외가 발생하면 postHandler를 호출하지 않고, afterCompletion호출 뒤 WAS에 예외 전달ExceptionResolver
적용 후에는 ExceptionResolver
에서 예외 해결 후 render 호출하고 afterCompletion 호출하고 정상응답으로 나갈 수 있음ExceptionResolver
로 예외를 해결해도 postHandle() 은 호출되지 않음MyHandlerExceptionResolver.java
@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");
//IllegalArgumentException가 발생하면 400오류로 바꾸겠다!
response.sendError(HttpServletResponse.SC_BAD_REQUEST,ex.getMessage());
return new ModelAndView();
//빈값으로 넘기면 정상적인 흐름으로 WAS까지 return이 됨
//500 error는 여기서 먹음
}
} catch (IOException e) {
log.error("resolver ex",e);
}
return null;
//null로 리턴 되면 원래 에러를 그냥 내보냄
}
}
ExceptionResolver
에서 ModelAndView
를 반환하면, 흐름이 정상흐름처럼 변경반환 값에 따른 동작 방식
ModelAndView
: new ModelAndView()
를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴ModelAndView
지정: ModelAndView 에 View , Model 등의 정보를 지정해서 반환하면 뷰를 렌더링ExceptionResolver
를 찾아서 실행하고, 처리할 수 있는 ExceptionResolver
가 없으면 기존에 발생한 예외를 내보냄WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
...
//HandlerExceptionResolver 등록
@Override
public void extendHandlerExceptionResolvers(
List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
}
HandlerExceptionResolver
를 등록 후 사용ExceptionResolver 활용
ExceptionResolver
를 활용하면 예외가 발생하면 WAS까지 예외가 던져지고, WAS에서 오류 페이지 정보를 찾아서 다시 /error
를 호출하는 과정을 생략할 수 있음ExceptionResolver
를 사용하면 컨트롤러에서 예외가 발생해도 ExceptionResolver
에서 예외를 처리ExceptionResolver
를 사용하면 예외처리가 깔끔@ExceptionHandler
을 처리예외에 따라서 HTTP 상태 코드를 지정
@ResponseStatus
가 달려있는 예외@ResponseStatus가 달려있는 예외
@ResponseStatus(code= HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException{
}
@ResponseStatus
애노테이션을 적용하면 ResponseStatusExceptionResolver가 HTTP 상태 코드를 변경해줌response.sendError(statusCode, resolvedReason)
를 호출sendError(400)
를 호출했기 때문에 WAS에서 다시 오류 페이지/error
를 내부 요청ResponseStatusException 예외
@ResponseStatus
는 개발자가 직접 변경할 수 없는 예외에는 적용할 수 없기 때문에 이때는 ResponseStatusException
예외를 사용
@GetMapping("/api/response-status-ex2")
public String responseStatusEx2() {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND,"error.bad",new IllegalAccessError());
}
TypeMismatchException
이 발생하는데, DefaultHandlerExceptionResolver
는 이것을 500 오류가 아니라 HTTP 상태 코드 400 오류로 변경 웹 브라우저에 HTML 화면을 제공할 때는 오류가 발생하면 BasicErrorController
를 사용하는게 편리하지만 API는 로 매우 세밀한 제어가 필요
API 예외처리의 어려운 점
@ExceptionHandler
라는 애노테이션을 사용하는 ExceptionHandlerExceptionResolver
을 기본으로 제공실행 흐름
1. Exception 예외가 터짐
2. ExceptionResolver를 통해 예외 해결 시도
3. 우선순위에 따라 ExceptionHandlerExceptionResolver가 먼저 실행
4. ExceptionHandlerExceptionResolver는 컨트롤러에 해당 예외를 처리할 수 있는 @ExceptionHandler
가 있으면 이 메서드를 대신 호출해줌
5. @RestController
나 @ResponseBody
등 코드에 적혀있는 특성이 다 적용이 됨
6. 바로 정상흐름으로 바꿔서 정상적으로 return
7. 여기서 정상흐름으로 끝났기 때문에 다시 서블릿 컨테이너로 올라가지 않음
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@ResponseStatus(HttpStatus.BAD_REQUEST)
//정상흐름으로 반환되기때문에 상태코드가 200이 되어서 이를 바꿔주기 위해 사용
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illigalExHandler(IllegalArgumentException e){
log.error("[exceptionHandler] ex",e);
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){
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({AException.class, BException.class})
@ControllerAdvice
또는 @RestControllerAdvice
를 사용하면 정상 코드와 예외 처리 코드를 분리할 수 있음
@Slf4j
@RestControllerAdvice //@ResponseBody+@ControllerAdvice
public class ExControllerAdvice {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illigalExHandler(IllegalArgumentException e){
log.error("[exceptionHandler] ex",e);
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){
log.error("[exceptionHandler] ex",e);
return new ErrorResult("EX","내부 오류");
}
}
@ControllerAdvice
에 대상을 지정하지 않으면 모든 컨트롤러에 적용(글로벌 적용)@RestControllerAdvice
= @ControllerAdvice
+ @ResponseBody
대상 컨트롤러 지정 방법
// 특정 애너테이션이 있는 컨트롤러에 적용
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// 해당 패키지 포함 하위 패키지에 있는 컨트롤러에 적용
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// 직접 컨트롤러 지정
@ControllerAdvice(assignableTypes = {ControllerInterface.class,
AbstractController.class})
public class ExampleAdvice3 {}