-
API 예외 처리Java/SpringBoot 2022. 8. 18. 17:24
기존 방식의 예외처리로는 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 {}
'Java > SpringBoot' 카테고리의 다른 글
파일 업로드 (0) 2022.08.18 타입 컨버터 (0) 2022.08.18 예외 처리와 오류 페이지 (0) 2022.08.18 의존성 주입 Dependency Injection. DI (0) 2021.02.24 [Thymeleaf] 문법 정리 (0) 2021.01.18