들어가기 앞서 ..
여태 스터디 프로젝트에서 진행하며 예외처리는 모두 try-catch를 이용해서 해왔다. 또한 학과 수업으로 배울때도 예외처리는 모두 try-catch를 사용했다. 이번 스터디를 진행하며 처음으로 @RestControllerAdvice를 접하게 되었다. 해당 내용에 대해 공부를 하다보니 다들 공통적으로 하는 말이 있었다. try-catch를 사용해서 코드가 복잡해지고 가독성이 떨어졌다는 것이였다. 한번도 try-catch문의 단점에 대해 생각해본적이 없었다. 실제로 RestControllerAdvice가 더 효율적인지 공부를 해보며 정리해보겠다.
@RestControllerAdvice?
스프링에는 예외처리를 해주는 다양한 방식들이 존재한다. 그중에서 오늘은 @RestControllerAdvice에 대해 알아보겠다.
앞서 말한것처럼 Java에서는 예외 처리를 위해 try-catch를 사용해줬다. 하지만 모든 코드에 try-catch를 붙여주는 것은 효율적이지 못하다. 공통으로 발생하는 예외를 모든 곳에 작성해줘야 하기 때문이다. 이를 위해 Spring에서는 에러처리라는 공통 관심사를 메인 로직에서 분리하는 다양한 예외 처리 방식을 지원해줬다. 여기서 HandlerExceptionResolver 인터페이스가 만들어진 것이다. 우선 HandlerExceptionResolver에 대해 간단하게 알아보겠다.
HandlerExceptionResolver?
쉽게 말해서 스프링에서 예외처리를 담당하는 인터페이스이다. 해당 인터페이스를 구현하면 개발자가 커스텀한 예외 처리 로직을 정의할 수 있다. 해당 인터페이스의 주요 목적은 컨트롤러에서 발생한 예외를 적절하게 처리하고, 클라이언트에게 요청에 대한 적절한 응답을 전달하는 것이다. 이러한 HandlerExceptionResolver를 사용하게 된다면 발생한 예외를 catch하고 HTTP 상태나 응답메시지를 설정할 수 있다.
스프링은 ExceptionResolver를 동작시키기 위해서는 아래와 같은 도구들을 사용한다.
- ResponseStatus
- ResponseStatusException
- ExceptionHandler
- ControllerAdvice, RestControllerAdvice
위의 내용에 대해서 순차적으로 알아보겠다.
ResponseStatus
@ResponseStatus는 컨트롤러의 메서드 또는 예외 클래스에 적용하여 HTTP 응답의 상태 코드를 지정할 수 있다. 해당 어노테이션을 사용하게 된다면 특정 메서드의 실행 결과나 예외 발생 시 반환되는 HTTP 응답의 상태 코드를 명시적으로 지정할 수 있다. 이것을 통해 클라이언트에게 적절한 응답 상태 코드를 제공할 수 있고, 예외 처리 결과를 명확하게 할 수 있다. 해당 어노테이션은 다음과 같은 경우에 적용이 가능하다.
- Exception 클래스 자체
- 메소드에 @ExceptionHandler와 함께
- 클래스에 @RestControllerAdivce와 함께
아래와 같이 만들어준 예외 클래스에 value = ~~ 와 같이 응답 상태 코드를 지정해줄 수 있다.
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class NoSuchElementFoundException extends RuntimeException {
...
}
ResponseStatusException
외부 라이브러리에서 정의된 코드는 우리가 @ResponseStatus를 붙여줄 수 없다. 이러한 경우에도 예외 처리가 가능하도록 ResponseStatusException이 생겨났다. ResponseStatsuException은 HttpStatus와, reason, cause를 추가해줄 수 있다. 또한 언체크 예외를 상속받고 있어서 명시적으로 에러를 처리해주지 않아도 된다는 특징이 있다.
여기서 말하는 언체크 예외란, 예외 처리를 강제하지 않는 예외를 말하는데, RuntimeException 클래스를 상속하는 예외 클래스들을 포함한다. 대표적으로, NullPointerException, IllegalArgumentException, IndexOutOfBoundsException 등이 있다. 주로 예상치 못한 상황이나 복구할 수 없는 오류를 나타내는 경우에 사용된다.
// 상태코드만 설정
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
// 상태코드와 메시지 설정
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid request");
// 상태코드와 메시지와 예외원인 설정
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "An error occurred", ex);
위의 두가지의 경우인 ResponseStatus, ResponseStatusExcpetion은 모두 직접적으로 예외 처리를 해주기에 일관된 예외 처리가 어렵고, 코드가 중복된다는 단점이 있다. 코드의 가독성이나 유지보수 측면에서 효율적이지 못하다는 것이다. 또한 예외가 WAS까지 전달되는 문제점이 있다.
@ExceptionHandler
@ExceptionHandler는 아주 유연하게 에러를 처리할 수 있는 방법을 제공한다. 해당 어노테이션은 컨트롤러 내에서의 특정 예외를 처리하기 위해서 사용된다. 또한 해당 어노테이션을 사용하게 되면 예외가 발생했을 때 해당 예외를 처리하는 메서드를 지정할 수 있다. 해당 어노테이션은 다음과 같은 위치에 사용이 된다.
- 컨트롤러의 메서드
- @ControllerAdvice, @RestControllerAdvice가 있는 클래스 메서드
@RestController
@RequiredArgsConstructor
public class OwnerController {
private final OwnerService ownerService;
...
@ExceptionHandler(NoSuchElementFoundException.class)
public ResponseEntity<String> handleNoSuchFoundException(NoSuchElementFoundException exception){
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exception.getMessage());
}
}
@ExceptionHandler는 Exception 클래스들을 속성으로 받아서 처리할 예외를 지정할 수 있다. 만약, ExceptionHandler 어노테이션에 예외 클래스를 지정하지 않는다면, 파라미터에 설정된 에러 클래스를 처리하게 된다. @ExceptionHandler는 @ResponseStatus와 달리 에러 응답을 자유롭게 할 수 있다는 점에서 장점이 있다.
해당 @ExceptionHandler를 사용 시에 해당 어노테이션에 등록한 예외 클래스와 파라미터로 받는 예외 클래스가 동일해야한다. 만약 값이 다르다면 런타임 시점에서 에러가 발생한다.
@ControllerAdvice, @RestControllerAdvice
@ControllerAdvice, @RestControllerAdvice는 전역적으로 예외를 처리할 수 있는 어노테이션이다. 각각 Spring3.2, Spring4.3부터 제공해주고 있다. @Controller 어노테이션이 붙은 컨트롤러에서 발생하는 예외를 처리할 수 있다.
두 어노테이션의 차이점은 무엇일까?
쉽게 말하면 @RestController, @Controller의 차이점과 같다. @RestControllerAdivce는 @ResponseBody가 추가로 붙어있어서 응답을 JSON형식으로 내려준다는 특징이 있다.
@ControllerAdvice는 여러 컨트롤러에 대해 전역적으로 ExceptionHandler를 적용해준다. 또한 해당 클래스에는 어노테이션으로 @Component가 있어서 ControllerAdvice가 선언된 클래스는 스프링 빈으로 등록된다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NoSuchElementFoundException.class)
protected ResponseEntity<?> handleNoSuchElementFoundException(NoSuchElementFoundException e) {
final ErrorResponse errorResponse = ErrorResponse.builder()
.code("Item Not Found")
.message(e.getMessage()).build();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}
}
스프링은 예외를 미리 처리해주는 기능을 제공하는 ResponseEntityExceptionHandler 추상 클래스가 있다. 이미 ResponseEntityExceptionHandler에는 스프링 예외에 대한 ExceptionHandler가 모두 구현이 되어있다. 그렇기에 해당 클래스를 상속받아서 사용해줘도 된다. 만약, 해당 추상 클래스를 상속받지 않는다면 스프링 예외들은 DefaultHandlerExceptionResolver가 처리하게 되는데 그렇게 되면 예외 처리기가 달라지므로 클라이언트가 일관되지 못한 에러 응답을 받지 못한다. 그렇기에 ResponseEntityExceptionHandler를 상속받는 것이 더 권장된다.
이처럼 ControllerAdvice를 사용하게 되면 다음과 같은 장점들이 있다.
- 하나의 클래스로 모든 컨트롤러에 대해 전역적으로 예외처리가 가능해진다.
- 직접 정의한 에러 응답을 일관성있게 클라이언트에게 내려줄 수 있다.
- 별도의 try-catch를 사용해주지 않아서 코드의 가독성이 올라간다.