본 포스팅은 인프런 - 스프링 MVC 2편을 강의를 바탕으로 공부하고 정리한 글입니다.
웹 페이지가 아닌 API 예외는 어떻게 처리하는지 알아보자.
API는 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 내려줘야하므로, 생각할 내용이 더 많다.
API 예외처리 (기존 방법)
서블릿
우선 서블릿 오류 페이지 방식으로 예외처리를 해보자.
👉🏻 서블릿 오류페이지 등록
📂 WebServerCustomizer
@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
}
}
- WAS에 예외가 전달되거나, response.sendError()가 호출되면 위에 등록해준 예외 페이지 경로가 호출된다.
👉🏻 API 예외 컨트롤러
📂 api/ApiExceptionController
@RestController
public class ApiExceptionController {
@RequestMapping("/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;
}
}
👉🏻 테스트 (Postman)
- 정상 호출 (http://localhost:8080/api/members/spring)
- 예외 발생 호출 (http://localhost:8080/api/members/ex)
정상 요청인 경우 JSON 형식으로 데이터가 정상 반환되지만, 오류가 발생하는 요청인 경우 만들어둔 오류 페이지 HTML이 반환된다.
클라이언트는 정상 요청이든, 오류 요청이든 JSON이 반환되어야 한다.
이러한 문제를 해결하기 위해 오류 페이지 컨트롤러도 JSON 응답을 할 수 있도록 수정해줘야 한다.
👉🏻 오류페이지 컨트롤러 - API 응답 추가
📂 servlet/ErrorPageController
@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));
}
- @RequestMapping(..., produces=MediaType.APPLICATION_JSON_VALUE)
- 클라이언트가 요청하는 HTTP 헤더의 Accept 값이 application/json일 때 해당 메소드 호출
- 즉, 클라이언트가 받고 싶은 미디어 타입이 json일 경우 호출된다는 의미
- 응답 데이터를 위해 Map을 만들고 status, message 이름으로 값을 할당해준다.
- Jackson 라이브러리는 Map을 JSON 구조로 변환
- ResponseEntity를 반환하기 때문에 메시지 컨버터가 동작하면서 클라이언트에 JSON이 반환된다.
👉🏻 테스트 (Postman)
- JSON 데이터로 응답이 오는 것을 확인할 수 있다.
스프링 부트
API 예외 처리도 스프링 부트가 제공하는 기본 오류 방식(BasicErrorController)을 사용할 수 있다.
👉🏻 BasicErrorController 코드
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {}
- errorHtml() : 클라이언트 요청의 Accept 헤더 값이 text/html인 경우 호출되며, view를 반환한다.
- error() : 그 외의 경우 호출되며, ResponseEntity로 HTTP Body에 JSON 데이터를 반환한다.
👉🏻 서블릿 오류 페이지 제거 - BasicErrorController 사용하도록 변경
📂 WebServerCustomizer
//@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
}
}
- 서블릿에서 등록했던 WebServerCustomize의 @Component를 주석처리 해준다.
👉🏻 테스트 (Postman)
- JSON 데이터로 응답이 오는 것을 확인할 수 있다.
HTML 페이지 vs API 오류
스프링 부트가 제공하는 BasicErrorController는 HTML 페이지를 제공하는 경우에는 매우 편리하다.
HTML 파일만 만들어주면 4xx, 5xx 모두 잘 처리해준다.
하지만, API 예외는 API에서 발생하는 예외에 따라 결과가 달라질 수 있어 더욱 세밀하고 복잡하게 처리해줘야 한다.
따라서 BasicErrorController는 HTML 화면을 처리할 때 사용하고, API 오류는 뒤에서 설명할 @ExceptionHandler를 사용하자!
API 예외 처리 (권장 방법)
예외가 발생해 서블릿을 넘어 WAS까지 예외가 전달되면 서버 내부 오류라고 판단해 HTTP 상태코드가 500으로 처리된다.
이때 발생하는 예외에 따라 400, 404 등 상태코드를 다르게 처리하고 싶다면 어떻게 해야할까?
HandlerExceptionResolver 소개
스프링 MVC는 컨트롤러(핸들러) 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있도록 HandlerExceptionResolver를 제공한다. (줄여서 ExceptionResolver라 함)
💡 HandlerExceptionResolver 인터페이스
public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex);
}
- handler : 핸들러(컨트롤러) 정보
- Exception ex : 핸들러(컨트롤러)에서 발생한 예외
- 반환 값에 따른 동작 방식
- 빈 ModelAndView : 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다.
- ModelAndView 지정 : View, Model 등의 정보를 지정해서 반환하면 뷰를 렌더링 한다.
- null : 다음 ExceptionResolver를 찾아 실행한다. 만약 처리할 수 있는 ExceptionResolver가 없다면 예외 처리가 되지 않고, 서블릿 밖으로 던져진다.
💡 HandlerExceptionResolver의 동작 흐름
HandlerExceptionResolver 적용
예를 들어 IllegalArgumentException 예외가 발생 할 경우 HTTP 상태코드를 400으로 처리하고 싶다고 해보자.
ExceptionResolver 적용 전
👉🏻 API 예외 컨트롤러
📂 exception/api/ApiExceptionController
@RestController
public class ApiExceptionController {
@RequestMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
...
if (id.equals("bad")) {
throw new IllegalArgumentException("잘못된 입력 값");
}
return new MemberDto(id, "hello" + id);
}
...
}
- id가 bad로 요청이 왔을 때 IllegalArgumentException이 발생하도록 했다.
👉🏻 테스트 (Postman)
상태 코드가 500인 것을 확인할 수 있다.
ExceptionResolver 적용 후
👉🏻 예외 리졸버 생성
📂 exception/resolver/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를 반환하는 이유는 Exception을 처리해 정상 흐름처럼 변경하는 것이 목적이다.
- 여기서는 IllegalArgumentException이 발생하면 response.sendError(400)을 호출해 HTTP 상태 코드를 400으로 지정하고 빈 ModelAndView를 반환한다.
👉🏻 예외 리졸버 등록
📂 WebConfig
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
- extendHandlerExceptionResolvers()를 사용해 만들어준 ExceptionResolver를 등록해준다.
ExceptionResolver 등록 시, extendHandlerExceptionResolvers()가 아닌 configureHanlderExceptionResolvers()를 사용할 경우 스프링이 기본으로 등록하는 ExceptionResolver가 제거되므로 주의하자.
👉🏻 테스트 (Postman)
상태코드가 500에서 400으로 변환된 것을 확인 할 수 있다.
예외 바로 끝내기
ExceptionResolver를 활용하면 예외가 발생했을 때 WAS에서 오류 페이지 정보를 찾아 다시 /error를 호출하는 복잡한 과정 없이 ExceptionResolver에서 예외를 처리해버릴 수 있다.
따라서 예외가 발생해도 서블릿 컨테이너까지 예외가 전달되지 않고, 스프링 MVC에서 예외 처리가 끝이 난다.
결과적으로 WAS 입장에서는 정상 처리가 된 것이다.
이렇게 예외를 ExceptionResolver에서 모두 처리할 수 있다는 것이 핵심이다!
그럼 예제를 통해 알아보자.
👉🏻 사용자 정의 예외 추가
📂 exception/exception/UserException
@Slf4j
public class UserException extends RuntimeException {
public UserException() {
super();
}
public UserException(String message) {
super(message);
}
public UserException(String message, Throwable cause) {
super(message, cause);
}
public UserException(Throwable cause) {
super(cause);
}
protected UserException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
👉🏻 API 예외 컨트롤러
📂 exception/api/ApiExceptionController
public class ApiExceptionController {
@RequestMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id) {
...
if (id.equals("user-ex")) {
throw new UserException("사용자 오류");
}
return new MemberDto(id, "hello" + id);
}
...
}
- id가 user-ex로 요청이 오면 UserException이 발생하도록 한다.
이제 이 예외를 처리하는 ExceptionResolver를 만들어주도록 하자.
👉🏻 예외 리졸버 생성
📂 exception/resolver/UserHandlerExceptionResolver
@Slf4j
public class UserHandlerExceptionResolver implements HandlerExceptionResolver {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
if (ex instanceof UserException) {
log.info("UserException resolver to 400");
String acceptHeader = request.getHeader("accept");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
if ("application/json".equals(acceptHeader)) {
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("ex", ex.getClass());
errorResult.put("message", ex.getMessage());
String result = objectMapper.writeValueAsString(errorResult);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(result);
return new ModelAndView();
} else {
// text/html
return new ModelAndView("error/500");
}
}
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
}
👉🏻 예외 리졸버 등록
📂 exception/WebConfig
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
...
resolvers.add(new UserHandlerExceptionResolver());
}
👉🏻 테스트 (Postman)
- Accept : application/json
- Accept : text/html
정리
- ExceptionResolver를 사용하면 컨트롤러에서 예외가 발생해도 ExceptionResolver에서 예외를 처리하기 때문에 서블릿 컨테이너까지 예외가 전달되지 않는다. → 결과적으로 WAS 입장에서는 정상처리!
- 블릿 컨테이너까지 예외가 전달되면 복잡한 추가 프로세스가 실행되지만 ExceptionResolver를 사용함으로써 예외처리가 상당히 깔끔해진다.
- 하지만 ExceptionResolver를 직접 구현하는 것이 상당히 복잡하다.
API 예외 처리의 어려운점
앞서 살펴본 BasicErrorController나 HandlerExceptionResolver를 직접 구현하는 방식으로 API 예외를 다루기는 쉽지 않다.
- HandlerExceptionResolver는 ModelAndView를 반환해야 한다. 하지만 이것은 API 응답에는 필요하지 않다.
- API 응답을 위해 HttpServletResponse에 직접 응답 데이터를 넣어줘야 했다. 이 과정은 상당히 번거롭다.
- 특정 컨트롤러에서만 발생하는 예외를 별도로 처리하기 어렵다.
따라서 스프링은 API 예외를 좀더 편하게 다룰 수 있도록 여러 ExceptionResolver들을 제공해준다.
API 예외처리 (최종 방법)
스프링이 제공하는 ExceptionResolver
스프링 부트가 기본으로 제공하는 ExceptionResolver는 다음과 같다.
우선 순위 |
종류 | 설명 |
1 | ExceptionHandlerExceptionResolver | @ExceptionHandler를 처리 API 예외 처리는 대부분 이 기능으로 해결 |
2 | ResponseStatusExceptionResolver | HTTP 상태 코드를 지정 예) @ResponseStatus(value = HttpStatus.NOT_FOUND) |
3 | DefaultHandlerExceptionResolver | 스프링 내부 기본 예외를 처리 |
* HandlerExceptionResolverComposite에 다음 순서대로 등록
ResponseStatusExceptionResolver
ResponseStatusExceptionResolver는 예외에 따라 HTTP 상태 코드를 지정해주는 역할을 하며, 다음 두가지 경우를 처리한다.
- @ResponseStatus가 있는 예외
- ResponseStatusException 예외
각각 예제를 통해 알아보자.
1️⃣ @ResponseStatus가 있는 예외
👉🏻 예외 생성
📂 exception/exception/BadRequestException
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException {
}
- BadRequestException 예외가 발생해 컨트롤러 밖으로 넘어가면 ResponseStatusExceptionResolver가 @ResposeStatus 어노테이션을 확인해 오류 코드를 400으로 변경하고, 오류 메시지를 담는다.
- ResponseStatusExceptionResolver 코드를 확인하면 결국 response.sendError(statusCode, resolvedReason)를 호출한다. sendError()를 호출했기 때문에 WAS에서 다시 오류 페이지(/error)를 내부 요청한다.
👉🏻 API 예외 컨트롤러 - 추가
📂 exception/api/ApiExceptionController
// @ResponseStatus 예외
@GetMapping("/response-status-ex1")
public String responseStatusEx1() {
throw new BadRequestException();
}
👉🏻 테스트 (Postman)
- 상태코드가 500이 아닌 400으로 변환된 것을 확인 할 수 있다.
- 오류 메시지 정보도 확인할 수 있다.
💡 ResponseStatusExceptionResolver의 메시지 기능
📂 exception/exception/BadRequestException
//@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad")
- reason을 MessageSource에서 찾는 기능도 제공한다.
📂 resource/messages.properties
error.bad=잘못된 요청 오류입니다. 메시지 사용
👉🏻 테스트 (Postman)
이때 @ResponseStatus는어노테이션을 직접 넣어줄 수 없는 라이브러리의 예외 코드 같은 곳에는 적용할 수 없다.
또한 어노테이션을 사용하기 때문에 조건에 따라 동적으로 변경하는 것이 어렵다.
이런 경우에는 ResponseStatusException 예외를 사용하면 된다.
2️⃣ ResponseStatusException예외
👉🏻 API 예외 컨트롤러 - 추가
📂 exception/api/ApiExceptionController
// ResponseStatusException 예외
@GetMapping("/response-status-ex2")
public String responseStatusEx2() {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
}
👉🏻 테스트 (Postman)
DefaultHandlerExceptionResolver
DefaultHandlerExceptionResolver는 스프링 내부에서 발생하는 스프링 예외를 해결한다.
예를 들어 파라미터 바인딩 시점에 타입이 맞지 않을 경우, 내부에서 TypeMismatchException이 발생한다. 이 때 예외가 WAS까지 전달되면 500 오류가 발생한다.
하지만 파라미터 바인딩은 대부분 클라이언트가 요청 정보를 잘못 입력했을 때 발생하는 문제로, 이런 경우 HTTP 상태 코드 400을 사용하도록 되어 있다. 이런 경우 DefaultHandlerExceptionResolver가 이것을 500 오류가 아닌 400 오류로 변경하는 역할을 한다.
예시 이외에도 DefaultHandlerExceptionResolver에는 스프링 내부 오류를 어떻게 처리할 지 수 많은 내용이 정의되어 있다.
DefaultHandlerExceptionResolver.handleTypeMismatch를 확인해보면 다음과 같은 코드를 확인할 수 있다.
• response.sendError(HttpServletResponse.SC_BAD_REQUEST)
즉, DefaultHandlerExceptionResolver도 결국 response.sendError()를 호출해 문제를 해결한다.
따라서 WAS에서 다시 오류 페이지(/error)를 내부 요청한다.
그럼 예제를 통해 확인해보도록 하자.
👉🏻 API 예외 컨트롤러
📂 api/ApiExceptionController
@GetMapping("/default-handler-ex")
public String defaultException(@RequestParam Integer data) {
return "ok";
}
- 요청 파라미터 data에 문자를 입력할 경우 내부에서 TypeMismatchException이 발생한다.
👉🏻 테스트 (Postman)
- 실행 결과 TypeMismatchException에 대해 HTTP 상태코드가 500이 아닌 400으로 처리된것을 확인 할 수 있다.
ExceptionHandlerExceptionResolver
스프링은 API 예외 처리 문제를 해결하기 위해 @ExceptionHandler라는 어노테이션을 제공한다.
이것이 바로 ExceptionHandlerExceptionResolver이며, 기본으로 제공하는 ExceptionResolver 중에 가장 우선순위가 높다.
실무에서 API 예외 처리는 대부분 이 기능을 사용한다!
💡 @ExceptionHandler 예외 처리 방법
- @ExceptionHandler 어노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해준다.
- 지정한 예외 또는 그 예외의 자식 클래스 예외는 모두 잡을 수 있다.
- 이때 더욱 자세한 것이 우선권을 가진다
- 부모, 자식 클래스에 대해 모두 정의되어 있을 때 자식예외가 발생하면 부모예외처리(), 자식예외처리() 둘다 모두 호출 대상이지만 더 자세한 것이 우선권을 가지므로 자식예외처리()가 호출된다.
@ExceptionHandler(부모예외.class)
public String 부모예외처리(부모예외 e) {}
@ExceptionHandler(자식예외.class)
public String 자식예외처리(자식예외 e) {}
- 다양한 예외를 한번에 처리할 수도 있다.
@ExceptionHandler({AException.class, BException.class})
public String ex(Exception e) {}
- 예외를 생략하는 것도 가능하다. 생략할 경우 메소드 파라미터의 예외가 지정된다.
@ExceptionHandler
public ResponseEntity<ErrorResult> useExHandle(UserException e) {}
- 스프링의 컨트롤러의 파라미터 응답처럼 다양한 파라미터와 응답을 지정할 수 있다.
그럼 예제를 통해 알아보자.
👉🏻 API 응답 객체 생성
📂 exhandler/ErrorResult
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
👉🏻 API 예외 컨트롤러 - V2 생성
📂 api/ApiExceptionV2Controller
@Slf4j
@RestController
@RequestMapping("/api2")
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("/members/{id}")
public ApiExceptionController.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 ApiExceptionController.MemberDto(id, "hello" + id);
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
👉🏻 테스트(Postman)
- llegalArgumentException 예외 발생
- UserException 예외 발생
- RuntimeException 예외 발생
💡 @ExceptionHandler 실행 흐름 정리
- 컨트롤러 호출 결과 IllegalArgumentException 예외가 컨트롤러 밖으로 던져진다.
- 예외가 발생했으므로 ExceptionResolver가 작동한다. 이때 가장 우선순위가 높은 ExceptionHandlerExceptionResolver가 실행된다.
- ExceptionHandlerExceptionResolver는 해당 컨트롤러에 IllegalArgumentException을 처리할 수 있는 @ExceptionHandler가 있는지 확인한다.
- illegalExHandle()를 실행한다. @RestController이므로 illegalExHandle()에도 @ResponseBody가 적용된다. 따라서 HTTP 컨버터가 사용되고, 응답이 JSON으로 반환된다.
- 이때 @ResponseStatus(HttpStatus.BAD_REQUEST)로 지정했기 때문에 HTTP 상태코드는 400으로 응답한다.
@ControllerAdvice
@ExceptionHandler를 사용해 예외를 깔끔하게 처리할 수 있지만, 정상 코드와 예외 처리 코드가 하나의 컨트롤러에 섞여있다. @ControllerAdivice 또는 @RestControllerAdvice를 사용하면 둘을 분리할 수 있다.
💡 @ControllerAdvice 특징
- @ControllerAdvice는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여해주는 역할을 한다.
- @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 {}
그럼 예제로 알아보자
👉🏻 예외 처리 코드분리
📂 exhandler/advice/ExControllerAdvice
@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", "내부 오류");
}
}
👉🏻 API 예외 컨트롤러 - @ExceptionHandler 모두 제거
📂 api/ApiExceptionV2Controller
@Slf4j
@RestController
@RequestMapping("/api2")
public class ApiExceptionV2Controller {
@GetMapping("/members/{id}")
public ApiExceptionController.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 ApiExceptionController.MemberDto(id, "hello" + id);
}
@Data
@AllArgsConstructor
static class MemberDto {
private String memberId;
private String name;
}
}
👉🏻 테스트(Postman)
- http://localhost:8080/api2/members/bad
- http://localhost:8080/api2/members/user-ex
- http://localhost:8080/api2/members/ex
정리
@ExceptionHandler와 @ControllerAdvice를 조합해 사용하면 API 예외를 깔끔하게 해결할 수 있다!
'🌱 Spring > Web MVC' 카테고리의 다른 글
스프링 타입 컨버터 (0) | 2022.05.25 |
---|---|
예외처리, 오류 페이지 (0) | 2022.05.16 |
로그인 (필터, 인터셉터) (0) | 2022.05.10 |
로그인 (쿠키, 세션) (0) | 2022.05.02 |
Bean Validation (0) | 2022.04.30 |
Validation (0) | 2022.04.26 |
타임리프(Thymeleaf) (1) | 2022.03.23 |
스프링 MVC 웹 페이지 만들기 (0) | 2022.03.22 |