Spring/Spring Boot

[Spring] Exception 예외 처리 - AOP와 @RestControllerAdvice를 이용한 ErrorHandling

민돌v 2023. 3. 6. 20:31
이전의 작성된 글을 조금 수정하여, Spring 에서 throw 로 던져지는 예외들을 전역적으로 잡아 처리하는 방법에 대해 설명합니다.
  • 예전 팀프로젝트를 할때 팀원분이 AOP를 이용한 예외처리를 한적이있는데,
  • 후에 다른기업 코테 과제테스트를 하다가, try catch 문을 이용해서 예외처리를 하니 코드가 너무 길어지고 보기가싫어서 
  • @ResControllerAdvice 어노테이션을 이용한 AOP 예외처리 에러핸들링 방법에대해 공부했던 적이 있습니다.

 

다시 사이드프로젝트를 진행하면서 예전 레거시 포스팅(?) 을 리펙토링한다는 생각으로 작성해봅니다!

 

 

 

[목차]

  1. Spring 에서 Exception 을 전역적으로 처리하는 과정
  2. @ControllerAdvice 와 @RestControllerAdvice
  3.  @ExceptionHandler
  4.  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 와 RestControllerAdvice

+

@ControllerAdvice 의 속성 설정을 통하여 원하는 컨트롤러나 패키지만 선택할 수 있습니다.

따로 지정을 하지 않으면 모든 패키지에 있는 컨트롤러를 담당하게 됩니다.

위 두 어노테이션 모두 적용 범위를 클래스나 패키지 단위로 제한하기 위해서는 아래와 같이 사용하면 됩니다.

https://velog.io/@banjjoknim/RestControllerAdvice
https://velog.io/@banjjoknim/RestControllerAdvice

 

이렇게 하위 로직에서 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 로 가는거니?? 에 대해서 공부해보고자합니다!

끝!

 

 

 

 

 


참고