API 예외 처리
기존 방식의 예외처리로는 API 요청 시
예외가 발생하지 않을 경우- 데이터가 정상적으로 반환되지만
예외가 발생하면- 오류 페이지를 반환한다.
그래서 오류 발생 시에도 JSON 형식의 데이터를 반환하도록 한다.
API 응답
@RequestMapping(value = "/error-page/500", produces =MediaType.APPLICATION_JSON_VALUE)
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));
}
서블릿에서 status, message를 받아서 HashMap으로 반환한다.
스프링부트 기본 오류 처리
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {}
클라이언트 http request의 Accept헤더가:
- text/html 이면 errorHtml()이 호출되어 html 반환
- 그 외의 경우 error() 이 호출되어 ResponseEntity 반환
HandlerExceptionResolver
발생하는 예외에 따라서 다른 상태코드로 처리하고, 오류 메시지와 형식을 API별로 커스터마이징 하고싶다.
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if (id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
if (id.equals("bad")) {
throw new IllegalArgumentException("잘못된 입력 값");
}
return new MemberDto(id, "hello " + id);
}
pathvariable에 따라 다른 예외를 throw 하도록 만들었다. 이제 bad를 파라미터로 넣어주면 500 에러가 뜬다.
IllegalArgumentException은 잘못된 요청으로 인한 에런데 서버에서 처리하지 못해서 500으로 넘어갔다.
이 경우 예외를 해결하고 동작을 처리할 방법이 필요하다.
실행 시점
1. preHandle(): 핸들러 이전
2. postHandle(): 핸들러 이후
3. aferCompletion(): 모든 처리가 끝난 이후
controller에서 예외가 전달되면 ExceptionResolver에서 ModelAndView를 반환한다.
ExceptionResolver는 WebConfigurer를 통해 등록한다.
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
SpringBoot가 제공하는 ExceptionResolver
1. ExceptionHandlerExceptionResolver
2. ResponseStatusExceptionResolver
3. DefaultHandlerExceptionResolver
의 순서로 등록된다.
ExceptionHandlerExceptionResolver: @ExceptionHandler 처리. API 예외 대부분 처리 가능.
ResponseStatusExceptionResolver: Http 상태코드 지정
DefaultHandlerExceptionResolver: 스프링 내부 기본 예외 처리
1. ResponseStatusExceptionResolver
package hello.exception.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}
@ResponseStatus 애노테이션으로 Http상태코드를 변경해준다.
response.sendError(statusCode, resolvedReason)을 호출한다.
sendError를 호출해 WAS에서 다시 오류페이지를 호출한다.
package hello.exception.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
//@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad")
public class BadRequestException extends RuntimeException {
}
reason을 MessageSource에서 찾는 메시지 기능을 사용할 수 있다.
이런 @ResponseStatus는 개발자가 직접 변경할 수 없는 예외에는 사용할 수 없다. 애노테이션을 사용하기 때문에 조건에 따라 동적으로 변경하기도 어렵다. 그럴때는 ResponseStatusException 예외를 사용한다.
@GetMapping("/api/response-status-ex2")
public String responseStatusEx2() {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
}
2. DefaultHandlerExceptionResolver
스프링 내부 예외를 해결한다.
예) 파라미터 바인딩 시 타입이 맞지 않으면 TypeMismatchException이라는 내부 예외 발생
서블릿 컨테이너까지 예외가 올라가고 500 오류 발생
-> 클라이언트의 입력 오류에 의한 예외이므로 400 에러가 맞다.
DefaultHandlerExceptionResolver.handleTypeMismatch 의 코드를 보면
response.sendError(HttpServletResponse.SC_BAD_REQUEST) 를 확인할 수 있다.
-->sendError를 통해 문제를 해결함
HTML 화면 오류 VS API 오류
API는 시스템마다 응답의 모양, 스펙 등이 모두 다르다. 같은 예외라도 어느 컨트롤러에서 발생했느냐에 따라 다르게 내려줘야 할 수도 있다.
@ExceptionHandler
package hello.exception.exhandler;
import hello.exception.exception.UserException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@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", "내부 오류");
}
@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 애노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 된다. 해당 컨트롤러에서 예외가 발생하면 이 메서드가 호출된다. 참고로 지정한 예외 또는 그 예외의 자식 클래스는 모두 잡을 수 있다.
@ControllerAdvice
대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여한다.
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class,
AbstractController.class})
public class ExampleAdvice3 {}