HTML 페이지의 경우 4xx, 5xx와 같은 오류 페이지만 있으면 대부분의 문제를 해결할 수 있다.

API는 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 내려주어야 한다.

ExceptionResolver

HandlerExceptionResolverComposite 에 다음 순서로 등록

  1. ExceptionHandlerExceptionResolver

    @ExceptionHandler 을 처리한다. API 예외 처리는 대부분 이 기능으로 해결

  2. ResponseStatusExceptionResolver

    HTTP 상태 코드를 지정

  3. 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 애노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 된다. 해당 컨트롤러에서 예외가 발생하면 이 메서드가 호출된다. 참고로 지정한 예외 또는 그 예외의 자식 클래스는 모두 잡을 수 있다.

실행 흐름

  1. 컨트롤러를 호출한 결과 IllegalArgumentException 예외가 컨트롤러 밖으로 던져진다.
  2. 예외가 발생했으로 ExceptionResolver 가 작동한다. 가장 우선순위가 높은 ExceptionHandlerExceptionResolver 가 실행된다.
  3. ExceptionHandlerExceptionResolver 는 해당 컨트롤러에 llegalArgumentException 을 처리할 수 있는 @ExceptionHandler 가 있는지 확인한다.
  4. illegalExHandle() 를 실행한다. @RestController 이므로 illegalExHandle() 에도 @ResponseBody 가 적용된다. 따라서 HTTP 컨버터가 사용되고, 응답이 다음과 같은 JSON으로 반환 된다.
  5. @ResponseStatus(HttpStatus.BAD_REQUEST) 를 지정했으므로 HTTP 상태 코드 400으로 응답한다.