[Spring] Exception 예외 처리 - AOP와 @RestControllerAdvice를 이용한 ErrorHandling
이전의 작성된 글을 조금 수정하여, Spring 에서 throw 로 던져지는 예외들을 전역적으로 잡아 처리하는 방법에 대해 설명합니다.
- 예전 팀프로젝트를 할때 팀원분이 AOP를 이용한 예외처리를 한적이있는데,
- 후에 다른기업 코테 과제테스트를 하다가, try catch 문을 이용해서 예외처리를 하니 코드가 너무 길어지고 보기가싫어서
- @ResControllerAdvice 어노테이션을 이용한 AOP 예외처리 에러핸들링 방법에대해 공부했던 적이 있습니다.
다시 사이드프로젝트를 진행하면서 예전 레거시 포스팅(?) 을 리펙토링한다는 생각으로 작성해봅니다!
[목차]
- Spring 에서 Exception 을 전역적으로 처리하는 과정
- @ControllerAdvice 와 @RestControllerAdvice
- @ExceptionHandler
- AOP 관점 예외처리 방법
👏🏻 1. Spring 에서 Exception 을 Catch 하는 과정 요약
1) 먼저 @ExceptionHandler 는 @Controller 가 적용된 Bean 에서 발생하는 예외를 잡아 하나의 메서드에서 처리하는 기능을 수행합니다.
- 이렇게 되며 @ExceptionHanlder가 적용된 handler 메소드에서 발생한 RuntimeException에 대해서 @ExceptionHandler 가 동작하지만
- @ExceptionHandler가 정의되어있지않은 다른 메소드(noHandler) 에서 RuntimException이 터지는 경우 잡아내지 못합니다.
- 즉, 컨트롤러마다 중복된 @ExceptionHandler를 정의해주어야 모든 에러에대한 커버가 가능해집니다.
@Controller
public class SimpleController {
// ...
@ExceptionHandler(value = RuntimeException.class)
public ResponseEntity<String> handle(IOException ex) {
// ...
}
public ResponseEntity<String> noHandler(IOException ex) {
// ...
}
}
이렇게 되며 @ExceptionHanlder가 적용된 handler 메소드에서 발생한 RuntimeException 이 발생되면
2) @ControllerAdvice
- 그렇기 때문에 우리는 @ControllerAdvice 를 사용합니다.
- @ControllerAdivice는 @Controller 어노테이션이 적용된 모든 곳에서의 발생되는 예외에 대해 Catch 합니다.
- 그렇기 때문에 @ControllerAdvice가 적용된 클래스에 정의되어있는 @ExceptionHandler 는 모든 컨트롤러에서 발생하는 에러에 대해 동작합니다.
@ControllerAdvice
public class PageControllerAdvice {
@ExceptionHandler(DataAccessException.class)
public ResponseEntity<String> dataExceptionHandle() {
return ResponseEntity.badRequest().build();
}
@ExceptionHandler(SQLException.class)
public ResponseEntity<String> sqlExceptionHandle() {
return ResponseEntity.status(INTERNAL_SERVER_ERROR).build();
}
}
→ 이렇게 정의한다면 모든 Controller 에서 발생하는 DataAccessException 과 SQLException 에 대해 해당 메서드가 실행됩니다.
2. @ControllerAdvice 와 @RestControllerAdvice
@ControllerAdvice는 @Controller 어노테이션이 있는 모든 곳에서의 예외를 잡을 수 있도록 해줍니다.
@ControllerAdivce 안에 선언된 @ExceptionHandler는 모든 컨트롤러에서 발생하는 예외상황을 잡을 수 있습니다.
1) @ControllerAdvice
- @ControllerAdvice 어노테이션은 @ExceptionHandler, @ModelAttribute, @InitBinder 가 적용된 메서드들에 AOP를 적용해 Controller 단에 적용하기 위해 고안된 어노테이션이라고 합니다.
- 클래스에만 선언하면 되며, 모든 @Controller에 대한 전역적으로 발생할 수 잇는 예외를 잡아서 처리해줍니다.
- @Componet가 선언되어있어 빈으로 관리되어 사용됩니다.
2) @RestControllerAdvice
- @RestControllerAdvice와 @ControllerAdvice의 차이는 @RestControllerAdvie = @ControllerAdvice + @ResponseBody 라는 것 입니다.
- 따라서 @RestControllerAdvie로 선언하면 따로 @ResponseBody를 붙혀주지 않아도 객체를 리턴할 수 있습니다.
- 에러 메세지를 DTO 객체에 담아 리턴해 주는 방식으로 사용됩니다.
+
@ControllerAdvice 의 속성 설정을 통하여 원하는 컨트롤러나 패키지만 선택할 수 있습니다.
따로 지정을 하지 않으면 모든 패키지에 있는 컨트롤러를 담당하게 됩니다.
위 두 어노테이션 모두 적용 범위를 클래스나 패키지 단위로 제한하기 위해서는 아래와 같이 사용하면 됩니다.
이렇게 하위 로직에서 Throw Exception 이 던져지게 되면, 상위로 타고타고 올라가 Controller 까지 왔을때 @ControllerAdvice 이 에러를 잡아서 @ExceptionHandler 에 선언된 Exception 인지 체크합니다.
3. @ExceptionHandler
실질적으로 예외를 잡아서 처리해주는건 @ExceptionHandler 어노테이션입니다.
@ExceptionHandler란 뭘까?
- @ExceptionHandler 어노테이션을 메서드에 선언하고 특정 예외 클래스를 지정해주면 해당 예외가 발생했을 때 메서드에 정의한 로직을 처리를 할 수 있습니다.
- 한마디로, 내가 처리하고 싶은 Exception을 정의한 다음 해당 예외가 발생시, 내 마음데로 처리가 가능하다는거!
AOP를 이용한 예외처리 방식이기때문에, 각 메서드 마다 try catch할 필요없이 깔금한 예외처리가 가능합니다.
- @ControllerAdvice 또는 @RestControllerAdvice에 정의된 메서드가 아닌 일반 컨트롤러 단에 존재하는 메서드에 선언할 경우, 해당 Controller에만 적용된다.
- (Controller, RestController에만 적용이 가능하다(@Service 등의 빈에서는 안된다!)
간단하게 사용방법을 작성하자면 아래같은 방식으로 사용할 수 있습니다.
ResponseErrorDto
- 먼저 에러 메세지를 담을 DTO 클래스를 생성해 줍니다.
@Getter
@RequiredArgsConstructor
public class ErrorResponse {
private final LocalDateTime timestamp;
private final int status;
private final String error;
private final String message;
private final String path;
@Builder(access = AccessLevel.PRIVATE)
private ErrorResponse(final int status, final String error, final String message,
final String path) {
this.timestamp = LocalDateTime.now();
this.status = status;
this.error = error;
this.message = message;
this.path = path;
}
public static ErrorResponse of(final HttpStatus httpStatus, final String message,
final HttpServletRequest httpServletRequest) {
return ErrorResponse.builder()
.status(httpStatus.value())
.error(httpStatus.getReasonPhrase())
.message(message)
.path(httpServletRequest.getRequestURI())
.build();
}
}
ErrorExceptionController
- 그 다음 모든 @Controller 빈에서 던져지는 에러에대해 전역적으로 처리해줄 @ExceptionHandler 를 @ControllerAdivce 가 정의된 클래스에 정의합니다. (작성방법에는 표현하고 싶은 데이터를 여러방법으로 담으면되니 선택!)
@RestControllerAdvice
public class HttpErrorAdvice {
//내가 주로 사용하는 형식 에러가 났던 url 같이 같이 보냄
@ExceptionHandler
public ResponseEntity<ErrorResponse> handlerException(final IllegalArgumentException e, final HttpServletRequest request) {
return ResponseEntity.badRequest()
.body(ErrorResponse.of(BAD_REQUEST, e.getMessage(), request));
}
/*common error*/
@ExceptionHandler(value = {BindException.class})
public ResponseEntity<ErrorResponse> errorHandler(BindException e) {
return ResponseEntity.badRequest().body(
new ErrorResponse(e.getAllErrors().get(0).getDefaultMessage(), 400)
);
}
@ExceptionHandler(value = {IllegalArgumentException.class})
public ResponseEntity<ErrorResponse> errorHandler(IllegalArgumentException e) {
return ResponseEntity.badRequest().body(
new ErrorResponse(e.getMessage(), 400)
);
}
@ExceptionHandler(value = {NullPointerException.class})
public ResponseEntity<ErrorResponse> errorHandler(NullPointerException e){
return ResponseEntity.badRequest().body(
new ErrorResponse(e.getMessage(), 400)
);
}
/*Http error*/
@ExceptionHandler(value = {HttpClientErrorException.class})
public ResponseEntity<ErrorResponse> errorHandler(HttpClientErrorException e){
return ResponseEntity.badRequest().body(
new ErrorResponse(e.getMessage(), 404)
);
}
@ExceptionHandler(value = {HttpServerErrorException .class})
public ResponseEntity<ErrorResponse> errorHandler(HttpServerErrorException e){
return ResponseEntity.badRequest().body(
new ErrorResponse(e.getMessage(), 500)
);
}
@ExceptionHandler(value = {UnknownHttpStatusCodeException .class})
public ResponseEntity<ErrorResponse> errorHandler(UnknownHttpStatusCodeException e){
return ResponseEntity.badRequest().body(
new ErrorResponse(e.getMessage(), 400)
);
}
}
그럼이제 이렇게 보였던 에러 메세지가 → 오른쪽처럼 커스텀하고 더 명확하게 핸들링해서 요청자에게 응답메세지를 보내줍니다.
다음 포스팅에서는 궁금했던 내가 던진 에러.. 너 어떻게 @ExceptionHandler 로 가는거니?? 에 대해서 공부해보고자합니다!
끝!
참고
- 익셉션 핸들러와 컨트롤러 어드바이스 : https://tecoble.techcourse.co.kr/post/2021-05-10-controller_advice_exception_handler/
- 디스페쳐 서블릿 동작과정 : https://mangkyu.tistory.com/216