@ExceptionHanlder 처리와 @ RestControllerAdvice ResponseEntityExceptionHandler
SrpingBoot 환경에서 간단한 사용자 회원가입과 로그인을 구현하는 restAPI 를 만들다가, 예외처리를 위해 @ExceptionHandler 를 사용하던 도중 @RestControllerAdvice 와 ResponseEntityExceptionHandler 관련한 내용에 대해 궁금증이 생겨 찾아본 결과를 포스팅 하고자 한다.
사용한 스프링 부트 버전은 다음과 같다.
작업 환경
spring boot 3.2.2
1. @ControllerAdvice 와 RestControllerAdvice 의 차이
우선 나는 RestControllerAdvice 를 사용해서 관리할 ExceptionHandler 들을 하나의 파일로 묶었다. 이 때 ControllerAdvice 와 RestControllerAdvice 의 차이가 궁금해서 찾아봤는데, @ResponseBody 가 추가적으로 붙어있었다. 둘중 어떤것을 사용하든 본인이 필요한대로 선언해서 사용하면 될 것 같다.
2. RestControllerAdvice
RestControllerAdvice 는 쉽게말해서 Exception 들을 관리해주는 Controller 라고 생각하면 된다. 이 RestControllerAdvice 를 사용하게되면 다음과 같은 작업을 스프링을 대신 해준다.
1) 대상으로 지정한 컨트롤러에 @ExceptionHandler @InitBinder 를 부여
2) 대상을 지정하지 않으면 모든 컨트롤러에 대해 자동 적용
만약 특정 클래스에 대해 대상을 지정하고 싶다면
@RestControllerAdvice( annotations = [지정컨트롤러명].class)
특정 패키지를 지정하고 싶다면
@RestControllerAdvie(basePackages = [지정할패키지명])
와 같이 사용하면 된다.
이 부분에 대해서도 지역적으로 에러처리를 나눌것인지, 따로 패키지나 클래스를 지정하지않고 쓸것인지에 대해 항상 고민하는 부분인데 ..
분리해서 구현하게 된다면 서비스 별로 리턴되는 에러 명세를 다르게 구성할 수 있다는 장점이 있겠지만, 한편으로는 나중을 생각했을 때 오히려 누락되거나 (패키지별로) 따로따로 관리를 해줘야해서 유지보수 편의성이 떨어질 수 있겠다고 생각했다. 장단점이 있어서 프로젝트의 성격에 맞게 선택해야 하는 문제같다. (항상 선택은 어렵다.)
진행하고 있는 API 프로젝트의 경우 사실 단순한 기능들만을 구현해두었기에 분리 필요성을 못느껴서 패키지를 따로 지정하진 않고 전역적으로 선언하고 진행했다.
사용 예시는 다음과 같다.
@Slf4j
@RestControllerAdvice
public class ExceptionAdviceHandler extends ResponseEntityExceptionHandler {
// RuntimException 처리
@ExceptionHandler
public ResponseEntity<Object> handleCustomException(ApiException ex) {
log.warn("RuntimeException ", ex);
ErrorCode errorCode = ex.getErrorCode();
return ResponseEntity.status(errorCode.getHttpStatus())
.body(makeErrorResponse(errorCode));
}
@ExceptionHandler
public ResponseEntity<Object> handleNoSuchAlgorithmException(NoSuchAlgorithmException ex) {
log.warn("NoSuchAlgorithmException ", ex);
ErrorCode errorCode = ApiErrorCode.INTERNAL_SERVER_ERROR;
return ResponseEntity.status(errorCode.getHttpStatus())
.body(makeErrorResponse(errorCode));
}
@ExceptionHandler
public ResponseEntity<Object> handleSignatureException(SignatureException ex) {
log.warn("SignatureException ", ex);
ErrorCode errorCode = ApiErrorCode.INVALID_TOKEN;
return ResponseEntity.status(errorCode.getHttpStatus())
.body(makeErrorResponse(errorCode));
}
}
3. ResponseEntityExceptionHandler
내가 해당 핸들러를 처음 접한건 이분 글을 읽고나서다.
https://mangkyu.tistory.com/205.
[Spring] @RestControllerAdvice를 이용한 Spring 예외 처리 방법 - (2/2)
예외 처리는 robust한 애플리케이션을 만드는데 매우 중요한 부분을 차지한다. Spring 프레임워크는 매우 다양한 에러 처리 방법을 제공하는데, 앞선 포스팅에서 @RestControllerAdvice를 사용해야 하는
mangkyu.tistory.com
보면 정리가 아주 잘 되어있는데 내가 지금까지 읽었던 @RestControlellerAdvice 를 사용해서 @ ExceptionHandler 를 사용하는 예제들에선 잘 쓰지 않았던 ResponseEntityExceptionHandler 를 상속해서 사용했다. 예제를 그냥 따라하기 보다는 내가 필요한 부분만 쓰고 싶었기 때문에 ResponseEntityExceptionHandler 가 뭔지 찾아봤다.
ResponseEntityExceptionHandler 은 Spring 에서 미리 자주쓰는 @ExceptionHandler 를 선언해놔서, 상속만 받으면 기본적인 예외처리는 자동으로 진행해주는 추상 클래스다. 기본적으로 리턴되는 스펙은 ResponseEntity 이고 선언되어 있는 Exception 은 생각보다 많기 때문에 사실 직접 찾아보는게 제일 낫다. 참고 용으로만 아래 사진을 참고해주면 되겠다.
내가 하고 싶었던 것은 스프링에서 기본으로 제공하는 응답이아닌 메시지 , 내가 직접 생성한 에러코드와 메시지 였기 때문에
@RestControllerAdvice 내에 각 Exception 별로 지정된 ( ex. handler[ExceptionName] ) 함수를 오버라이드 해서 사용 했다. ResponseEntityExceptionHandler 를 상속한 상태에서 RestControllerAdvice 내에
@ExceptionHandler 에 동일한 Exception 을 지정하면 당연하게도 bean 충돌 오류가 발생하므로, 선언되어 있는 함수만 @Override 해야한다.
예시로 MethodArgumentNotValid 오류에 대해 응답을 원하는대로 변경하기 위해 아래 함수를 오버라이딩 했다.
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
log.warn("handleMethodArgumentNotValid", ex);
ErrorCode errorCode = ApiErrorCode.INVALID_PARAM;
return ResponseEntity.status(errorCode.getHttpStatus())
.body(makeErrorResponse(errorCode));
}
이렇게 사용하면 실행시 내가 지정한 에러코드로 정상적으로 리턴이 된다!
ResponseEntityExceptionHandler 클래스를 사용하지 않아도 되지만 기본으로 제공하고 있는 Exception 들에 대해 자동으로 연결해주기 때문에, 나는 편리해서 사용했다.
Exception 처리는 비즈니스 로직보다도 항상 할 때마다 고민되는 것 같다.