-
@RestControllerAdvice에 대하여스터디 노트 2023. 10. 18. 14:04
Spring Boot를 사용하면 기본적으로 에러를 처리하는 BasicErrorController라는 녀석이 있습니다.
이 컨트롤러는 Spring Boot를 사용하던 중 error가 발생할 경우 produces Type이 text/html일 경우 errorHtml이 호출됩니다.
여기서 RequestMapping의 produces는 contentType을 나타내는 속성입니다.
기본적으로 Spring Boot를 활용하면 별도의 에러 페이지 설정 없이도 error ModelAndView를 활용할 수 있습니다.
다만 우리가 직접 예외를 핸들링하고 싶을땐, Controller를 만들어 errorHtml() 메소드와 error()메소드를 직접 재정의하여 사용하면 됩니다.
하지만 url이 외부에 노출되기 때문에 에러 페이지에 누구나 접근이 가능하다는 단점이 있습니다.
또한 컨트롤러에 각각 페이지에 해당하는 exception handling code가 작성되게 되면 관리 포인트의 분산으로 코드의 중복이 발생할 수 있다는 단점도 존재합니다.
그렇기에 우리는@RestControllerAdvice를 활용하여 Exception 관리를 보다 효율적으로 할 수 있습니다.
📌 @RestControllerAdvice 와 @ControllerAdvice의 차이점
Exception을 핸들링할 수 있는 annotation의 종류는 @RestControllerAdvice와 @ControllerAdvice가 존재합니다.
우선 둘의 차이점은 무엇일까요?
먼저 @ControllerAdvice부터 정리해보도록 하겠습니다.
우선 ControllerAdvice Annotation의 생김새는 이렇습니다.
정식 문서의 설명을 확인해보겠습니다.
굉장히 깁니다..요약해보자면
- @ControllerAdvice를 통해 스프링 빈 컨테이너에 스캐닝 대상이 될 수 있습니다.
- @Controller가 선언된 클래스의 예외처리에 대응할 수 있습니다.
- 모든 빈은 Ordered 시맨틱스 또는 @Order / @Priority 선언에 따라 정렬되며, Ordered 시맨틱스가 @Order / @Priority 선언보다 우선합니다. @ControllerAdvice 빈은 그런 다음 런타임에 그 순서대로 적용됩니다.
- @ControllerAdvice가 선언된 클래스 내부의 @ExceptionHandler는 예외처리자로 해당 예외가 발생하게 되면 그 예외를 처리하는 기능을 수행합니다.
- @ControllerAdvice는 전역적으로 선언이 되기 때문에 문서상에서는 annotations()나 basePackageClasses() 및 basePackage()로 범위를 더 좁혀서 사용하라고 권장을 하고 있습니다.
즉, @ControllerAdvice를 사용하면 예외 처리를 보다 신속하고 손쉽게 전역적으로 해낼 수 있습니다.
그럼 다음으로 알아보고자 했던 @RestControllerAdvice를 확인해보겠습니다.
사실 REST라는 문구가 붙은 것을 통해 유추해낼 수 있는 부분인데, RestControllerAdvice의 내부에는 @ControllerAdvice와 @ResponseBody가 기본적으로 선언되어 있음을 알 수 있습니다.
덕분에 @RestControllerAdvice가 선언된 Handler클래스에서는 굳이 @ExceptionHandler가 @ResponseBody를 선언하지 않아도 Json형식으로 데이터를 전송할 수 있게 됩니다.
📌 어떻게 쓰면 되나?
다음 예제들을 통해 ControllerAdvice를 어떻게 사용하면 되는지 확인해보겠습니다.
Lombok과 Spring Boot Web 의존성만 추가된 간편한 예제 프로젝트를 하나 만들었습니다.
그리고 패키지 구조를 다음과 같이 만들었습니다.
먼저 ErrorCode interface입니다.
각각의 ExceptionCode들에 상속하여 HttpStatus와 message getter를 구현하도록 처리할 것입니다.
// ErrorCode interface public interface ErrorCode { HttpStatus getHttpStatus(); String getMessage(); }
다음은 ErrorCode interface를 구현하는 각각의 ExceptionCode Enum을 만들어줍니다.
이 Enum을 통해 각각의 Error를 우리 서버에 맞게 커스터마이징한 내용으로 바인딩하여 사용할 것입니다.
@Getter @RequiredArgsConstructor public enum ExceptionCode implements ErrorCode { // Exception Codes INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error...", HttpStatus.INTERNAL_SERVER_ERROR.value()), INTERNAL_IO_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server IO Exception...", HttpStatus.INTERNAL_SERVER_ERROR.value()), RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "Resource not found...", HttpStatus.NO_CONTENT.value()) ; private final HttpStatus httpStatus; private final String message; private final int code; }
다음은 ResponseBody에 실어서 반환해줄 ErrorResponse 클래스를 만들어줍니다.
@Getter public class ErrorResponse { private int code; private String message; private ErrorResponse(ErrorCode errorCode) { this.code = errorCode.getHttpStatus().value(); this.message = errorCode.getMessage(); } public static ErrorResponse of(ErrorCode errorCode) { return new ErrorResponse(errorCode); } }
생성자는 외부에서 생성하지 못하게 private으로 선언을 해놓고 ErrorResponse.of()를 통해서만 생성할 수 있도록 처리하였습니다.
그리고 @RestControllerAdvice를 선언할 Handler클래스를 작성해줍니다.
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(NullPointerException.class) protected ResponseEntity<ErrorResponse> handlerNullPointerException(NullPointerException e) { return ResponseEntity.badRequest().body(ErrorResponse.of(ExceptionCode.RESOURCE_NOT_FOUND)); } }
NullPointerException에 대해서 badRequest로 body에 우리가 지정한 ErrorResponse를 지정하여 리턴해주는 역할을 합니다. 현재 여기서는 INTERNAL_SERVER_ERROR Code를 바인딩하여 넘기도록 되어 있습니다.
이제 외부에서 호출할 API를 만들어줍니다.
@RestController @RequiredArgsConstructor @RequestMapping("/advice") public class AdviceController { @GetMapping("/test") public ResponseEntity adviceTest() { throw new NullPointerException(); } }
@Controller가 선언된 클래스에서 발생하는 Exception에 대해 전역적으로 반응을 하기 때문에 Controller에서 별도의 로직 없이 바로 NullPointerException()을 발생시켰습니다.
자 준비는 모두 끝났습니다.
혹시라도 우리가 Advice를 구현하지 않은 상태에서 NullPointerException이 발생하게 된다면 어떻게 될까요?
서버 로그에 NullPointerException을 찍어주고 사용자에게는 Whitelabel Error Page를 전달해줍니다.
뭐 충분히 에러 전달은 가능하지만 커스터마이징이 불가능하기에 500에 Internal Server Error가 발생하게 됩니다.
만약 RestControllerAdvice를 통해 ExceptionHandler를 구현한다면 어떻게 처리가 될까요?
우리가 구현한 GlobalExceptionHandler를 통해 NullPointerException이 감지되었다는 문구와 함께, 커스터마이징한 메시지가 전달되게 됩니다.
위에 Enum에서 설정했던 'Resource not found...'가 메시지로 찍혀있는 것을 볼 수 있습니다.
이렇듯 각각의 Exception 상황에서 조금 더 구체적이고 효과적인 로깅을 전달하고 싶다면 ControllerAdvice와 ExceptionHandler를 구현하여 처리가 가능합니다.
만약 메시지를 상황에 따라 바꾸고 싶다면 어떻게 할까요?
먼저 상황에 따라 변경할 수 있도록 ErrorResponse에 updateMessage()를 통해 message를 변경할 수 있도록 만들어줍니다.
(사실 setter 대신에 사용되는 것이기에 절대로 권장하지는 않습니다.)@Getter public class ErrorResponse { ... public void updateMessage(String message) { this.message = message; } }
이렇게 메시지를 변경할 수 있는 기능을 추가시킨 다음 GlobalExceptionHandler 클래스에 ExceptionHandler를 구현하여 커스터마이징 message를 전달해줍니다.
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { ... @ExceptionHandler(IllegalArgumentException.class) protected ResponseEntity<ErrorResponse> handlerIllegalArgumentException(IllegalArgumentException e) { log.error(e.getLocalizedMessage()); ErrorResponse errorResponse = ErrorResponse.of(ExceptionCode.INTERNAL_IO_ERROR); errorResponse.updateMessage("일부러 발생시킨 IllegalException"); return ResponseEntity.badRequest().body(errorResponse); } }
그리고 컨트롤러를 호출하게 되면 다음과 같은 결과를 확인할 수 있습니다.
📌 마치며
@ControllerAdvice 들을 통해 Exception을 핸들링할 경우 사용하면 좋을 것 같습니다.
무의미한 Internal Server Error의 반복적 반환보다는 조금 더 확실하고 명확하게 Error Message를 로깅하여 트러블슈팅에 도움이 될 수 있도록 프로젝트를 구성할 수 있습니다.
그리고 무엇보다 정형화된 Exception을 사용할 수 있게되어 조금 더 공통적이고 심플한 구조로 Exception handling이 가능해집니다.
'스터디 노트' 카테고리의 다른 글
@RequiredArgsConstructor 가 하는 일 (1) 2023.10.18 스프링 빈 스코프에 대하여 (0) 2023.10.18 [Java] 불변 단 건 리스트는 Collections.singletonList() 를 활용해보세요. (0) 2023.10.17 Java Optional의 메소드 사용 설명서(Optional 잘 활용하기) (1) 2023.10.16 [Java] IntStream 메소드 사용해보기 (0) 2023.10.16