ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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
Designed by Tistory.