HTML 페이지의 경우 4xx, 5xx와 같은 오류 페이지만 있으면 대부분의 문제를 해결할 수 있다.
API는 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 내려주어야 한다.
ExceptionResolver
HandlerExceptionResolverComposite 에 다음 순서로 등록
ExceptionHandlerExceptionResolver
@ExceptionHandler 을 처리한다. API 예외 처리는 대부분 이 기능으로 해결
ResponseStatusExceptionResolver
HTTP 상태 코드를 지정
DefaultHandlerExceptionResolver 우선 순위가 가장 낮다
ResponseStatusExceptionResolver
다음 두 가지 경우를 처리한다.
@ResponseStatus 가 달려있는 예외
ResponseStatusException 예외
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}
BadRequestException 예외가 컨트롤러 밖으로 넘어가면 ResponseStatusExceptionResolver 예외가 해당 애노테이션을 확인해서 오류 코드를 HttpStatus.BAD_REQUEST (400)으로 변경하고, 메시지도 담는다.
내부에서 response.sendError(statusCode, resolvedReason) 를 호출.
@GetMapping("/api/response-status-ex1")
public String responseStatusEx1() {
throw new BadRequestException();
}
DefaultHandlerExceptionResolver
스프링 내부에서 발생하는 스프링 예외를 해결.
파라미터 바인딩 시점에 타입이 맞지 않으면 내부에서 TypeMismatchException 이 발생하는데, 이 경우 예외가 발생했기 때문에 그냥 두면 서블릿 컨테이너까지 오류가 올라가고, 결과적으로 500 오류가 발생.
이런 문제를 해결하여 HTTP 상태 코드를 500에서 400으로 변경해준다.
@GetMapping("/api/default-handler-ex")
public String defaultException(@RequestParam Integer data) {
return "ok";
}
@ExceptionHandler
ApiExceptionV2Controller
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@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;
}
}
@ControllerAdvice
정상 코드와 예외 처리 코드를 분리할 수 있다.
@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", "내부 오류");
}
}
@ExceptionHandler 애노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 된다. 해당 컨트롤러에서 예외가 발생하면 이 메서드가 호출된다. 참고로 지정한 예외 또는 그 예외의 자식 클래스는 모두 잡을 수 있다.
실행 흐름
IllegalArgumentException 예외가 컨트롤러 밖으로 던져진다.ExceptionResolver 가 작동한다. 가장 우선순위가 높은
ExceptionHandlerExceptionResolver 가 실행된다.ExceptionHandlerExceptionResolver 는 해당 컨트롤러에 llegalArgumentException 을 처리할 수 있는 @ExceptionHandler 가 있는지 확인한다.illegalExHandle() 를 실행한다. @RestController 이므로 illegalExHandle() 에도 @ResponseBody 가 적용된다. 따라서 HTTP 컨버터가 사용되고, 응답이 다음과 같은 JSON으로 반환 된다.@ResponseStatus(HttpStatus.BAD_REQUEST) 를 지정했으므로 HTTP 상태 코드 400으로 응답한다.