API 예외 처리는 어떻게 해야할까? 🤔
HTML
오류 페이지 vs API
오류 메시지400
, 4xx
등) 오류 페이지를 만들어서 고객에게ApiExceptionController
- API
예외 컨트롤러@Slf4j
@RestController
public class ApiExceptionController {
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
if (id.equals("ex")) {
throw new RuntimeException("잘못된 사용자");
}
return new MemberDto(id, "hello " + id);
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
id
값이 ex
인 경우 예외가 발생하도록 했다.✔ 정상 호출
JSON 형식으로 데이터가 반환된다.
✔ 예외 발생 호출
JSON이 아닌, HTML 오류 페이지가 반환된다.
→ 우리가 원하는 것은 정상 요청이든, 오류 요청이든 JSON이 반환되는 것!
ErrorPageController
- 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));
}
produces = MediaType.APPLICATION_JSON_VALUE
: 클라이언트가 요청하는 HTTP Header의 Accept
값이 application/json
일 때 해당 메서드를 호출한다는 뜻이제 예외가 발생했을 경우도 (요청 HTTP Header의 Accept
값이 JSON
이면),
JSON 형식으로 반환되는 것을 알 수 있다!
API 예외 처리도 스프링 부트가 제공하는 기본 오류 방식을 사용할 수 있다.
BasicErrorController
를 사용하도록
WebServerCustomizer
의@Component
를 주석처리하자!
스프링부트는 BasicErrorController
가 제공하는 기본 정보들을 활용해서 오류 API를 생성해준다.
HTML
페이지 vs API
오류BasicErrorController
를 확장하면 JSON 메시지도 변경할 수 있다.
4xx
, 5xx
등 모두 잘 처리해준다.BasicErrorController
만으로는 부족하다😥📌 정리
BasicErrorController
는 HTML 화면을 처리할 때 사용하고,
API는@ExceptionHandler
를 사용하자!
HandlerExceptionResolver
시작HandlerExceptionResolver
스프링MVC는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우,
예외를 해결하고, 동작을 새로 정의할 수 있도록 HandlerExceptionResolver
를 제공한다.
(HandlerExceptionResolver
를 줄여서 ExceptionResolver
라고 한다.)
ExceptionResolver
적용 전postHandler
를 호출하지 않는다! ExceptionResolver
적용 후HandlerExceptionResolver
가 추가되어postHandle()
은 호출❌ HandlerExceptionResolver
- 인터페이스public interface HandlerExceptionResolver {
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}
handler
: 핸들러(컨트롤러) 정보Exception ex
: 핸들러(컨트롤러)에서 발생한 예외MyHandlerExceptionResolver
@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");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView();
}
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
}
ExceptionResolver
는 ModelAndView
를 반환하는데, (try
~catch
를 하듯이)Exception
을 처리해서 정상 흐름처럼 변경하는 것이 목적이기 때문이다!IllegalArgumentException
이 발생하면 responsesendError(400)
을 호출해서400
으로 지정하고 빈 ModelAndView
를 반환한다.DispatcherServlet
의 동작 방식ModelAndView
: 뷰 렌더링❌, 정상 흐름으로 서블릿이 리턴된다.ModelAndView 지정
: 뷰를 렌더링한다.null
: 다음 ExceptionResolver
를 찾아서 실행한다. ExceptionResolver
가 없으면 예외 처리가 안되고,ExceptionResolver
활용response.sendError(xxx)
호출로 변경해서 서블릿에서 상태 코드에 따른 오류를 처리하도록 위임ModelAndView
에 값을 채워서 예외에 따른 새로운 오류 화면 뷰 렌더링해서 고객에게 제공/api/members/ex
/api/members/bad
HandlerExceptionResolver
활용예외가 발생하면 WAS까지 예외가 던져지고, WAS에서 오류 페이지 정보를 찾아서 다시 /error
를 호출하고.. 지금까지의 과정은 너무 복잡했다!😣
ExceptionResolver
를 활용해서 이런 복잡한 과정 없이 깔끔하게 문제를 해결해보자!
🔗 전체 코드
HTTP 요청 해더의 ACCEPT
값이
application/json
→ JSON으로 오류를 내려주고, error/500
에 있는 HTML 오류 페이지를 보여준다.📌 정리
ExceptionResolver
를 사용하면 컨트롤러에서 예외가 발생해도ExceptionResolver
에서 예외 처리를 한다!
이렇게 예외를 이곳에서 모두 처리할 수 있다는 것이 핵심이다!
ExceptionResolver1
ExceptionResolver
를 사용하면 모든 예외를 이 곳에서 처리할 수 있어서 좋다고 했다!
하지만 ExceptionResolver
를 직접 구현하는 과정은 꽤 복잡했다😢
이번에는 스프링이 제공하는 ExceptionResolver
에 대해 알아보자!
ExceptionResolver
💡
HandlerExceptionResolverComposite
에 다음 순서로 등록
ExceptionHandlerExceptionResolver
ResponseStatusExceptionResolver
DefaultHandlerExceptionResolver
→ 우선 순위가 가장 낮다
ExceptionHandlerExceptionResolver
: @ExceptionHandler
을 처리한다. ResponseStatusExceptionResolver
: HTTP 상태 코드를 지정해준다.DefaultHandlerExceptionResolver
: 스프링 내부 기본 예외를 처리한다.ResponseStatusExceptionResolver
예외에 따라서 HTTP 상태 코드를 지정해준다.
다음 두 가지 경우를 처리한다.
@ResponseStatus
가 달려있는 예외ResponseStatusException
예외@ResponseStatus
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 오류 요청")
public class BadRequestException extends RuntimeException{
}
BadRequestException
예외가 컨트롤러 밖으로 넘어가면 ResponseStatusExceptionResolver
예외가 해당 애노테이션을 확인해서 오류 코드를 HttpStatus.BAD_REQUEST
으로 변경하고, 메시지도 담는다.ResponseStatusExceptionResolver
는 response.sendError(statusCode, resolvedReason)
를 호출한다!실행 결과
reason
을 MessageSource
에서 찾는 기능도 제공한다.
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad")
public class BadRequestException extends RuntimeException{
}
messages.properties
에 error.bad=잘못된 요청 오류입니다. 메시지 사용
과 같이 메시지를 따로 뺄 수도 있다.ResponseStatusException
@ResponseStatus
는 개발자가 직접 변경할 수 없는 예외에는 적용할 수 없다! → 애노테이션을 직접 넣어야하기 때문!ResponseStatusException
예외를 사용하면 된다!@GetMapping("/api/response-status-ex2")
public String responseStatusEx2() {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException())
}
실행 결과
ExceptionResolver2
이번에는 DefaultHandlerExceptionResolver
에 대해서 알아보자!
DefaultHandlerExceptionResolver
스프링 내부에서 발생하는 스프링 예외를 해결한다.
파라미터 바인딩 시점에 타입이 맞지 않으면 내부에서 TypeMismatchException
이 발생하는 경우를 살펴보자!
500
오류가 발생한다!400
에 해당하는 오류이다!DefaultHandlerExceptionResolver
이 이것을 500 오류가 아니라 HTTP 상태 코드 400 오류로 변경한다.이처럼 DefaultHandlerExceptionResolver
은 스프링 내부 오류를 어떻게 처리할지 수많은 내용이 정의되어 있으며, 이에 맞는 동작을 해준다.
ApiExceptionController
코드 추가@GetMapping("/api/default-handler-ex")
public String defaultException(@RequestParam Integer data) {
return "ok";
}
Integet data
에 문자를 입력하면 내부에서 TypeMismatchException
이 발생한다.실행 결과
📌 정리
HandlerExceptionResolver
는 좋기는 하지만!
- 직접 사용하기에는 복잡하다.
- API 오류 응답의 경우
response
에 직접 데이터를 넣어야 해서 불편하다.ModelAndView
를 반환해야 하는 것도 API에는 잘 맞지 않는다.
그래서 스프링이 제공하는 아주 좋은 예외 처리 기능인 @ExceptionHandler
을 자주 사용한다!
사실 이것이 아직 알아보지 않은 ExceptionHandlerExcetpionResolver
이다. 😉
@ExceptionHandler
HTML
화면 오류 vs API
오류BasicErrorController
를 사용하는 것이 좋다.400
, 4xx
등 관련된 오류 페이지를 만들어서 고객에게 오류 화면을 보여주면 된다. → BasicErrorController
가 모두 구현해놓음!BasicErrorController
를 사용하거나 HandlerExceptionResolver
를 직접 구현하는 방식으로 API 예외를 다루기는 쉽지 않다.API
예외처리의 어려운 점HandlerExceptionResolver
는 API 응답에는 필요 없는,ModelAndView
를 반환한다.HttpServletResponse
에 직접 응답 데이터를 넣어주는 것은 매우 불편하다. @ExceptionHandler
스프링은 API 예외 처리 문제를 해결하기 위해 ExceptionHandlerExceptionResolver
을 제공한다!
💡
@ExceptionHandler
@ExceptionHandler
애노테이션을 사용하는 매우 편리한 예외 처리 기능!
ExceptionHandlerExceptionResolver
를 기본으로 제공하며, 기본으로 제공하는 ExceptionResolver
중에 우선순위도 가장 높다. @ExceptionHandler
예외 처리 방법@ExceptionHandler
애노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정!IllegalArgumentException
또는 그 하위 클래스까지 처리할 수 있다!@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
에 지정한 부모 클래스는 자식 클래스까지 처리할 수 있다.@ExceptionHandler(부모예외.class)
public String 부모예외처리()(부모예외 e) {}
@ExceptionHandler(자식예외.class)
public String 자식예외처리()(자식예외 e) {}
자식예외
발생 → 부모예외처리()
, 자식예외처리()
모두 호출 대상 → (자세한 것이 더 우선권을 가짐) 자식예외처리()
호출 부모예외
발생 → 부모예외처리()
만 호출 대상 → 부모예외처리()
호출@ExceptionHandler({AException.class, BException.class})
public String ex(Exception e) {
log.info("exception e", e);
}
@ExceptionHandler
에 예외를 생략할 수 있다. 생략하면 메서드 파라미터의 예외가 지정된다.@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {}
@ExceptionHandler
에는 다양한 파라미터와 응답을 지정할 수 있다.자세한 파라미터와 응답은 여기 참고!
IllegalArgumentException
처리@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
log.error("[exceptionHandle] ex", e);
return new ErrorResult("BAD", e.getMessage());
}
IllegalArgumentException
예외가 컨트롤러 밖으로 던져진다.ExceptionResolver
가 작동한다.ExceptionHandlerExceptionResolver
가 실행된다.ExceptionHandlerExceptionResolver
는 해당 컨트롤러에 IllegalArgumentException
을 처리할 수 있는 @ExceptionHandler
가 있는지 확인한다.illegalExHandle()
를 실행한다. @RestController
이므로 illegalExHandle()
에도 @ResponseBody
가 적용된다.@ResponseStatus(HttpStatus.BAD_REQUEST)
를 지정했으므로 HTTP 상태 코드 400으로 응답한다.@ControllerAdvice
스프링이 제공하는 편리한 예외 처리 기능인 @ExceptionHandler
을 이용해서 예외를 깔끔하게 처리할 수 있었지만, 정상코드와 예외 처리 코드가 하나의 컨트롤러에 섞여 있다.
→ @ControllerAdvice
또는 @RestControllerAdvice
를 사용해서 분리해보자!
@ControllerAdvice
@ControllerAdvice
는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler
, @InitBinder
기능을@ControllerAdvice
에 대상을 지정하지 않으면 모든 컨트롤러에 적용된다. (글로벌 적용)@RestControllerAdvice
는 @ControllerAdvice
와 같고, @ResponseBody
가 추가되어 있다.// 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 {}