본문 바로가기

[Spring] Spring Boot는 어떤 방법으로 예외 처리를 할까? @ExceptionHandler, @ControllerAdvice

민이(MInE) 2024. 4. 29.
반응형

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 | 김영한 - 인프런

김영한 | 웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습

www.inflearn.com

본 글은 인프런의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 참고하여 작성되었습니다~


 

저번 글에서는 Servlet이 예외 처리를 하는 흐름에 대해서 알아보았습니다. 그럼 오늘은 Spring Boot는 Servlet이 하는 예외 처리를 어떻게 깔끔하게 처리하는지 알아보겠습니다.

 

우선 Spring Boot는 오류 발생 시 /error 를 오류 페이지로 요청을 합니다. 이 경로는 application.properties에서 server.error.path 로 수정할 수 있습니다.

 

server.error.path=/error

 

우리가 API를 만들 때 오류 처리가 필요하면 매우 복잡하게 생각이 필요합니다. 어떤 기능을 하는 컨트롤러인지 어떤 응답인지에 따라서 응답을 다르게 출력해줘야 하기 때문입니다.

그럼 이제 Spring Boot에서 예외 처리를 어떻게 해야하는지 알아봅시다~

 

 

ExceptionResolver

 

Spring 은 ExceptionResolver를 이용해서 컨트롤러 밖으로 던져진 예외를 해결하고, 동작 방식을 변경할 수 있습니다.

 

 

 

ExceptionResolver의 반환 값에 따른 DispatcherServlet의 동작 방식은 아래 세가지가 있습니다.

 

  • return new ModelAndView(): 다음과 같이 아무것도 없는 ModelAndView를 반환하게 되면 정상 흐름으로 Servlet이 리턴됩니다.
  • return new ModelAndView("error/400"): 다음과 같이 뷰의 정보를 넣어주게 되면 뷰를 렌더링 합니다.
  • null: null을 반환하게 되면 다음 ExceptionResolver를 찾아서 반환합니다. 처리할 수 있는게 없다면 예외 처리가 되지 않고, 서블릿 밖으로 보내게 됩니다.

Spring Boot는 ExceptionResolver를 기본적으로 세 가지를 제공합니다.

 

  • ExceptionHandlerExceptionResolver
  • ResponseStatusExceptionResolver
  • DefaultHandlerExceptionResolver

위 세 가지 중에서 ExceptionHandlerExceptionResolver가 우선 순위가 가장 높고, DefaultHandlerExceptionResolver가 우선 순위가 가장 낮습니다. (Spring Boot는 항상 자세한 것이 우선 순위가 높습니다 ! )

 

 

DefaultHandlerExceptionResolver

 

일단 가장 우선 순위가 낮은 DeafultHandlerExceptionResolver 부터 살펴봅시다.

DefaultHandlerExceptionResolver의 예로는 TypeMismatchException이 있습니다.

 

 

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

 

위와 같이 작성하고 http://localhost:8080/api/default-handler-ex?data=hello 를 요청해보면, 400(Bad Request) 에러가 발생할 것 입니다.

Integer인데 hello를 보내줘서 예외가 발생하였기 때문에 500 오류가 발생해야 하지만 HTTP에서는 400 이 경우 400에러를 사용하도록 되어 있기 때문에 DefaultHandlerExceptionResolver는 400에러로 바꿔서 보여줍니다.

 

 

 

ResponseStatusExceptionResolver

 

다음으로 ResponseStatusExceptionResolver 입니다. ResponseStatusExceptionResolver는 @ResponseStatus가 붙어있거나 ResponseStatusException 예외를 처리합니다.

 

@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad")
public class BadRequestException extends RuntimeException {
}

(error.bad는 messages.properties를 만들어서 작성해줬습니다.)

 

위와 같이 작성하고 컨트롤러에서 호출해줍니다.

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

 

그리고 http://localhost:8080/api/response-status-ex1?message= 를 호출하면 400(Bad Request)가 발생할 것 입니다.

BadRequestException 예외가 컨트롤러 밖으로 나가면 ResponseStatusExceptioNResolver가 @ResponseStatus가 있는 것을 확인하고 오류 코드를 400으로 변경한 것입니다.

 

@ResponseStatus는 코드를 직접 작성하는 것이기 때문에 라이브러리 예외에는 처리하기가 힘듭니다. 이 경우에는ResponseStatusException을 사용할 수 있습니다.

 

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

 

 

 

@ExceptionHandler, @ControllerAdvice

 

이제 Spring Boot가 제공하는 @ExceptionHandler와 @ControllerAdvice를 사용해보겠습니다. 결론적으로 우리는 개발을 할 때 이것들만 잘 사용하면 됩니다.

 

우선 예외 발생 시 API 응답으로 사용할 객체를 만들어줍니다.

 

@Data
@AllArgsConstructor
public class ErrorResult {
    private String code;
    private String message;
}

 

그리고 예외 처리를 할 컨트롤러에서 @ExceptionHandler를 사용해줍니다.

@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", "내부 오류");
}

 

 

첫번째의 경우 IllegalArgumentException또는 그 하위 자식 클래스를 모두 처리 가능하고, 두 번째 경우는 메서드 파라미터의 예외가 지정됩니다. 그리고 앞에서 말했듯이 Spring  Boot는 자세한 것이 우선 순위가 높습니다.

 

그런데 @ExceptionHandler를 컨트롤러 내에서 처리하기에는 일반적인 코드와 섞여있을 수 있어 보기 안좋습니다. 그래서 @ControllerAdvice로 분리할 수 있습니다.

 

@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", "내부 오류");
    }
}

 

 

위와 같이 처리하면 모든 컨트롤러에 대해서 에외 처리를 하게 됩니다. @RestControllerAdvice(annotation = RestController.class)라고 하게 되면 @RestController가 달린 컨트롤러에 대해서만 처리하게 되고, @RestControllerAdvice(annotation = "org.example.controllers") 라고 하게 된다면 해당 패키지 내에 존재하는 컨트롤러에 대해서만 처리하게 되고, @RestControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class}) 라고 해주면 해당 클래스에 대해서만 처리해주게 됩니다.

 

오늘은 Spring Boot에서 예외 처리를 보기 좋게 해주기 위하여 @ExceptionHandler와 @ControllerAdvice에 대해서 알아보았습니다.

반응형

댓글