@ExceptionHandler, @ControllerAdvice를 활용한 API 예외 처리

Seungyeon Choi·2022년 5월 22일
0

스프링

목록 보기
2/2

why: API는 어떻게 예외처리 해야할까?!

API는 각 시스템마다

  • 응답의 모양이 다르고
    ex. 같은 예외가 발생하더라도 메뉴 api와 주문 api는 응답이 달라질 수 있음
  • 스펙도 다르다
    ex. 어떤 상황에선 예외지만, 어떤 상황에선 정상흐름일 수 있음

따라서, 세밀한 예외처리가 필요하다

@ExceptionHandler, @ControllerAdvice 없이, 세밀한 예외처리를 할 수 있을까?

BasicErrorController를 이용할 수 있을까?

BasicErrorController는 웹 브라우저에서 html 화면을 통해 예외 정보를 전달할 땐 편리하지만, api 응답으론 어렵다.

  • api서버가 표준화된 html에러를 만드는 것도 매번 정의하기 번잡스럽다
  • 받는쪽에서 html을 파싱해서 그 예외를 핸들링해야하는 것도 귀찮다.

HandlerExceptionResolver를 직접 구현하는건 어떨지?

  • ModelAndView를 반환 형태를 유지해야하는 점이 불편하다
  • API 응답에서는 response body에 직접 데이터를 쓰길 원한다.
    그럴라면 HttpServletResponse 객체에 write()함수 이용해서 써야하는데... 넘넘 불편하다
  • 일정 api와 캘린더 api에서 같은 runtimeexcepion을 다르게 처리하고 싶은데, 어떻게 구현할 수 있을지?
    handler 넘겨받으면 되겠지만.. 넘넘 불편하다

이런 문제를 어떻게 해결하지?!?!?

스프링이 이 귀찮은 작업을 대신 구현해두었다

what: @ExceptionHandler, @ControllerAdvice 로 처리하기

스프링 부트가 기본으로 제공하는 ExceptionResolver 중
가장 우선순위가 높은 ExceptionHandlerExceptionResolver가 이 귀찮음을 대신 해준다

@ExceptionHandler로 특정 클래스 내부에서 예외 처리하기

ExceptionHandlerExceptionResolver은 @ExceptionHandler 을 처리해주는 리졸버이다.

@ExceptionHandler같은 경우는 @Controller, @RestController가 적용된 Bean내에서 발생하는 예외를 잡아서 하나의 메서드에서 처리해주는 기능을 한다.

@ResponseStatus(HttpStatus.BAD_REQUEST) // 200 ok로 내려가지 않도록 추가
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e) { // 이 예외의 자식까지 처리해줌
    //ExceptionHandlerExceptionResolver가 처리해줌
    log.error("[exceptionHandler] e", e);
    return new ErrorResult("BAD", e.getMessage()); // json만들어서 반환해줌
    // @ResponseStatus 설정 없으면, 정상흐름(200ok)으로 바꿔서 응답 내려감.
    // 서블릿 컨테이너로 에러가 올라가지 않고, 여기서 흐름 끝남
}

이 예시를 살펴보면,

IllegalArgumentException 이 발생할 경우,

  • 400 bad request로 처리되고,
  • response body에 ErrorResult 객체가 json 형식으로 쓰이게 된다

참고할 점은

  • Controller, RestController에만 적용가능하다.
    • @Service같은 빈에서는 안됨
  • 리턴 타입은 자유롭게 해도 된다.
    • Controller내부에 있는 메서드들은 여러 타입의 response가 가능하다. 그 타입과 상관없이 리턴 타입이어을 줄 수 있다
  • Controller내부에 있을 경우, @ExceptionHandler를 등록한 Controller에만 적용된다.
    • 다른 Controller에서 NullPointerException이 발생하더라도 예외를 처리할 수 없다.
  • 메서드의 파라미터로 Exception을 받아오면, 그 예외를 지정해서 처리하겠단 의미가 된다
  • 구체적인 예외일 수록 우선순위가 높다.
  • 한번에 여러 예외를 처리할 수 있다

@ControllerAdvice로 글로벌 예외 처리하기

만약 @ExceptionHandler만 있다면, 컨트롤러 내에서 정상흐름과 예외처리 흐름을 동시에 관리해야 한다.
이것을 분리해서 관리할 수 없을까?!?!

이런 니즈를 해결하려면, @ControllerAdvice, @RestControllerAdvice(=@ControllerAdvice+@ResponseBody) 를 사용해야 한다.

위 어노테이션은
대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여한다
대상으로 지정하는 방법은 3가지가 있다

  • 특정 어노테이션이 있는 컨트롤러 지정
  • 특정 패키지를 지정(하위 패키지 포함)
  • 특정 클래스 지정

@ControllerAdvice에 대상을 지정하지 않으면, 전체 컨트롤러에 적용된다.(글로벌함)

how: 예시

@RestControllerAdvice(basePackages = "burrito.exception.api") // 대상을 지정하지 않으면, 모든 컨트롤러에 적용됨
//@ControllerAdvice(annotations = RestController.class) // 1. RestController 만 적용
//@ControllerAdvice("burrito.example.controllers") // 2. 특정 패키지와 그 하위에 적용
//@ControllerAdvice(assignableTypes = {MenuController.class,OrderController.class}) // 3. 특정 컨트롤러 클래스 지정
public class BurritoExceptionControllerAdvice {
 
    @ResponseStatus(HttpStatus.BAD_REQUEST) // 200 ok로 내려가지 않도록 추가
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandler(IllegalArgumentException e) { // 이 예외의 자식까지 처리해줌
        //ExceptionHandlerExceptionResolver가 처리해줌
        log.error("[exceptionHandler] e", e);
        return new ErrorResult("BAD", e.getMessage()); // json만들어서 반환해줌
        // @ResponseStatus 설정 없으면, 정상흐름(200ok)으로 바꿔서 응답 내려감.
        // 서블릿 컨테이너로 에러가 올라가지 않고, 여기서 흐름 끝남
    }
 
    @ExceptionHandler
    public ResponseEntity<ErrorResult> userExHandler(UserException e) { // 이 예외의 자식까지 처리해줌
        //ExceptionHandlerExceptionResolver가 처리해줌
        log.error("[exceptionHandler] e", e);
        ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
        return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
    }
 
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 200 ok로 내려가지 않도록 추가
    @ExceptionHandler // 예외 잡는 범위는 이 컨트롤러 한정임
    public ErrorResult exHandler(Exception e) { // 구체적으로 처리되지않은 예외들이 여기서 처리됨
        //ExceptionHandlerExceptionResolver가 처리해줌
        log.error("[exceptionHandler] e", e);
        return new ErrorResult("EX", "내부 오류"); //반환타입이 거의 컨트롤러 급으로 여러 가지임
    }
 
}

IllegalArgumentException 이 발생하면 어떻게 될까?

  1. 컨트롤러를 호출한 결과 IllegalArgumentException 예외가 컨트롤러 밖으로 던져진다
  2. 예외가 발생했으므로 → ExceptionResolver가 동작한다.
    • 가장 우선순위가 높은 ExceptionHandlerExceptionResolver가 실행된다
  3. ExceptionHandlerExceptionResolver는 해당 컨트롤러에 IllegalArgumentException를 처리할 수 있는 @ExceptionHandler가 있는지 확인한다
  4. illegalExHandler()을 실행한다
    • @RestControllerAdvice 이므로, illegalExHandler() 반환값에 @ResponseBody가 적용된다
  5. @ResponseBody가 적용되었으므로, HTTP 컨버터가 사용되고, 응답이 json객체로 변환되어 http response body에 쓰인다.
  6. @ResponseStatus(HttpStatus.BAD_REQUEST)가 있어서 상태코드는 400으로 반환된다
profile
캘린더 만드는 개발자입니당

0개의 댓글