예외 처리 2 / API

강한친구·2022년 6월 22일
0

Spring

목록 보기
25/27

API 오류

API오류는 각 오류상황에 맞는 응답스펙을 정하고, JSON 데이터를 내주어야한다.

    @GetMapping("/api/members/{id}")
    public MemberDto getMember(@PathVariable("id") String id) {
        if (id.equals("ex")) {
            throw new RuntimeException("잘못된 사용자");
        }

        return new MemberDto(id, "hello" + id);
    }

이러한 코드에서는 기존에 설정해둔 ErrorPage정보때문에 html이 반환이 된다.

따라서 Controller에서 JSON을 반환할 수 있도록 처리해야한다.

	@RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Map<String, Object>> errorPage500Api
            (HttpServletRequest request, HttpServletResponse response) {
        log.info("API Error Page 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));
    }

이런식으로 API오류를 보내줄 수 있는 Controller가 필요하다.
하지만 이런 방식은 너무 복잡하고 스프링부트에서 제공하는 방식을 사용하면 더 편리하다.

HTML vs API

BasicController를 확장하면 JSON 메시지를 변경할 수 있다. 하지만 이보다는 @ExceptionHandler 기능을 사용하는것이 더 편리하다.

API는 각각 API마다 다른 응답스펙을 가지고 있어야하고 이에 매우 세밀하고 복잡하게 구성딘다.

HandlerExceptionResolver

API오류를 어떻게 처리해야할까

예를 들어, IllegalArgumentException 을 처리하지 못해서 컨트롤러 밖으로 넘어가는 일이 발생하면 HTTP 상태코드를 400으로 처리하고 싶다. 어떻게 해야할까?

if (id.equals("bad")) {
            throw new IllegalArgumentException("잘못된 값");
        }

이런식으로 bad가 들어오면 Illegal을 던지게되면, 500이 터지게 된다. prehandle 까지만 작동하고 post까지 가지 않기 때문이다.

따라서 ExceptionResolver를 사용해서 예외해결을 시도하도록 해주면 된다. 이때 해결이 된다면 정상처리 되어서 반환되게 되는것이다.

MyHandlerException

@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        if (ex instanceof IllegalArgumentException) {
            log.info("IllegalArgumentException resolver to 400");
            try {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
                return new ModelAndView();
            } catch (IOException e) {
                log.error("resolver ex", e);
            }
        }
        return null;
    }
}

resolverException은 중간에 IllegalArguemntException이 발생하면, 이 Excpetion을 받아서 삼켜버리고, 내가 원하는 SC를 담고, (여기서는 SC.BAD_REQUEST). ModelAndView를 반환하면 된다. 빈 ModelAndView를 반환하면 뷰를 렌더링 하지않고, 정상 흐름으로 서블릿이 리턴된다.

활용

  • 예외 상태 코드를 변환 할 수 있다.
  • 뷰 템플릿 처리
    • 반환 ModelAndView에 View를 반환할 수 있다.
  • API 응답처리가 가능하다.

하지만 이렇게 일일히 ExceptionResolver를 만들면 너무 불편하다. 따라서 스프링 제공 Resolver를 쓰면 좋다

스프링 제공 Resolver

@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException{

}

ResponseStatus에서 Code를 지정해주면, Resolver가 이를 보고 해당 오류에 맞춰서 값을 처리해주게 된다.

    @GetMapping("/api/response-status-ex1")
    public String responseStatusEx1() {
        throw new BadRequestException();
    }

이런식으로 BadRequestEx를 던지는 api 컨트롤러는

이런 결과값을 가지게 되는 것이다.

ResponseStatusException

@ResponseStatus 는 개발자가 직접 변경할 수 없는 예외에는 적용할 수 없다. (애노테이션을 직접 넣어야 하는데, 내가 코드를 수정할 수 없는 라이브러리의 예외 코드 같은 곳에는 적용할 수 없다.)

추가로 애노테이션을 사용하기 때문에 조건에 따라 동적으로 변경하는 것도 어렵다. 이때는 ResponseStatusException 예외를 사용하면 된다

	@GetMapping("/api/response-status-ex2")
    public String responseStatusEx2() {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
    }

404가 잘 나오는것을 볼 수 있다.

DefaultHandlerExceptionResolver

DefaultHandlerExceptionResolver 는 스프링 내부에서 발생하는 스프링 예외를 해결한다. 대표적으로 파라미터 바인딩 시점에 타입이 맞지 않으면 내부에서 TypeMismatchException 이 발생하는데, 이 경우 예외가 발생했기 때문에 그냥 두면 서블릿 컨테이너까지 오류가 올라가고, 결과적으로500 오류가 발생한다.

파라미터 바인딩은 대부분 클라이언트가 HTTP 요청 정보를 잘못 호출해서 발생하는 문제이다. HTTP 에서는 이런 경우 HTTP 상태 코드 400을 사용하도록 되어 있다.
DefaultHandlerExceptionResolver 는 이것을 500 오류가 아니라 HTTP 상태 코드 400 오류로 변경해준다.

@GetMapping("/api/default-handler-ex")
    public String defaultException(@RequestParam Integer data) {
        return "ok";
    }

Integer형태의 data를 받아야하는 API이다. 이 API에 정상적으로 Integer값을 주면

문제가 없지만

이렇게 잘못된 값을 주면 400 오류를 내주는것을 볼 수 있다.

또 있다

스프링이 제공하는 Resolver는 또 있다.

  1. ExceptionHandlerExceptionResolver

  2. ResponseStatusExceptionResolver
    HTTP 응답 코드 변경

  3. DefaultHandlerExceptionResolver
    스프링 내부 예외 처리

지금까지는 2번과 3번만 써봤는데, 이번에는 ExceptionHanderExceptionResolver를 알아보도록 하겠다.

ExceptionHandler

지금까지 쓰던 Resolver는 너무 불편하다.
ModelAndView를 만들어주고 반환해주고 너무 복잡하고
또 서로 다른 방식으로 RuntimeException을 처리하려면 어떻게 해야할까?

이를 위해 ExceptionHandler가 존재한다

	@ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandler(IllegalArgumentException e) {
        log.error("[exceptionHandler] 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", "내부 오류");
    }

예외 처리 방법

@ExceptionHandler 어노테이션을 지정하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정하면, 해당 컨트롤러 안에서 최우선적으로 오류를 처리한다.

단점

그럼 이걸 모든 컨트롤러마다 다 적용해야하는가?
ControllerAdvice로 해결 할 수 있다

Controller Advice

@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandler(IllegalArgumentException e) {
        log.error("[exceptionHandler] 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", "내부 오류");
    }
}

이렇게 @RestControllerAdvice를 지정하면 한번에 모든 오류처리를 할 수 있다.

대상지정을 하지 않으면 어떤 컨트롤러에서 와도 다 처리해주며, 특정 컨트롤러에 적용하는것도 가능하다 .

// 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 {}

공식문서에 따르면 이런식으로 지정 가능하다 나와있다.

정리

API 오류처리를 하는 방법을 알아보았다

0개의 댓글