특정 클래스에 @RestControllerAdvice 애너테이션을 추가하면 여러 개의 Controller 클래스에서 @ExceptionHandler, @InitBinder 또는 @ModelAttribute가 추가된 메서드를 공유해서 사용할 수 있습니다.
@InitBinder와 @ModelAttribute 애너테이션은 JSP, Thymeleaf 같은 서버 사이드 렌더링(SSR, Server Side Rendering) 방식에서 주로 사용되는 방식입니다.
이 말의 의미를 예외 처리 관점에서 설명하자면,
@RestControllerAdvice 애너테이션을 추가한 클래스를 이용하면 예외 처리를 공통화할 수 있다는 것입니다.
Controller 클래스에서 @ExceptionHandler 로직 제거 : 이전 Controller에 구현된 @ExceptionHandler가 추가된 메서드들은 모두 제거합니다.
ExceptionAdvice 클래스 정의
@RestControllerAdvice
public class GlobalExceptionAdvice {
}
예외를 처리할 ExceptionAdvice 클래스에 @RestControllerAdvice 애너테이션을 추가하면 이 클래스는 이제 Controller 클래스에서 발생하는 예외를 도맡아서 처리하게 됩니다.
Exception 핸들러 메서드 구현 : GlobalExceptionAdvice 클래스에서 처리할 Exception 핸들러 메서드를 구현
@RestControllerAdvice
public class GlobalExceptionAdvice {
// (1)
@ExceptionHandler
public ResponseEntity handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
List<ErrorResponse.FieldError> errors =
fieldErrors.stream()
.map(error -> new ErrorResponse.FieldError(
error.getField(),
error.getRejectedValue(),
error.getDefaultMessage()))
.collect(Collectors.toList());
return new ResponseEntity<>(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);
}
// (2)
@ExceptionHandler
public ResponseEntity handleConstraintViolationException(
ConstraintViolationException e) {
// TODO should implement for validation
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
}
이처럼 @RestControllerAdvice 애너테이션을 이용해서 예외 처리를 공통화하면 각 Controller마다 추가되는 @ExceptionHandler 로직에 대한 중복 코드를 제거하고, Controller의 코드를 단순화할 수 있습니다.
ErrorResponse 수정
위 코드에서 URI 변수로 넘어오는 값의 유효성 검증에 대한 에러(ConstraintViolationException) 처리는 아직 구현되지 않았습니다.
ErrorResponse 클래스가 ConstraintViolationException에 대한 Error Response를 생성할 수 있도록 ErrorResponse 클래스를 수정해 보도록 하겠습니다.
@Getter
public class ErrorResponse {
private List<FieldError> fieldErrors; // (1)
private List<ConstraintViolationError> violationErrors; // (2)
// (3)
private ErrorResponse(List<FieldError> fieldErrors, List<ConstraintViolationError> violationErrors) {
this.fieldErrors = fieldErrors;
this.violationErrors = violationErrors;
}
// (4) BindingResult에 대한 ErrorResponse 객체 생성
public static ErrorResponse of(BindingResult bindingResult) {
return new ErrorResponse(FieldError.of(bindingResult), null);
}
// (5) Set<ConstraintViolation<?>> 객체에 대한 ErrorResponse 객체 생성
public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
return new ErrorResponse(null, ConstraintViolationError.of(violations));
}
// (6) Field Error 가공
@Getter
public static class FieldError {
private String field;
private Object rejectedValue;
private String reason;
private FieldError(String field, Object rejectedValue, String reason) {
this.field = field;
this.rejectedValue = rejectedValue;
this.reason = reason;
}
public static List<FieldError> of(BindingResult bindingResult) {
final List<org.springframework.validation.FieldError> fieldErrors =
bindingResult.getFieldErrors();
return fieldErrors.stream()
.map(error -> new FieldError(
error.getField(),
error.getRejectedValue() == null ?
"" : error.getRejectedValue().toString(),
error.getDefaultMessage()))
.collect(Collectors.toList());
}
}
// (7) ConstraintViolation Error 가공
@Getter
public static class ConstraintViolationError {
private String propertyPath;
private Object rejectedValue;
private String reason;
private ConstraintViolationError(String propertyPath, Object rejectedValue,
String reason) {
this.propertyPath = propertyPath;
this.rejectedValue = rejectedValue;
this.reason = reason;
}
public static List<ConstraintViolationError> of(
Set<ConstraintViolation<?>> constraintViolations) {
return constraintViolations.stream()
.map(constraintViolation -> new ConstraintViolationError(
constraintViolation.getPropertyPath().toString(),
constraintViolation.getInvalidValue().toString(),
constraintViolation.getMessage()
)).collect(Collectors.toList());
}
}
}
수정된 ErrorResponse는 총 두 개의 예외 유형을 처리해서 Error Response에 포함할 수 있습니다.
첫 번째가 DTO 클래스의 유효성 검증에서 발생하는 MethodArgumentNotValidException에 대한 Error Response이고
두 번째는 URI의 변수 값 검증에서 발생하는 ConstraintViolationException에 대한 Error Response입니다.
of() 메서드
of() 메서드는 Java 8의 API에서도 흔히 볼 수 있는 네이밍 컨벤션(Naming Convention)입니다.
주로 객체 생성 시 어떤 값들의(of~) 객체를 생성한다는 의미에서 of() 메서드를 사용합니다.
GlobalExceptionAdvice 클래스를 수정
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
final ErrorResponse response = ErrorResponse.of(e.getBindingResult());
return response;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleConstraintViolationException(
ConstraintViolationException e) {
final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());
return response;
}
}
전에는 ErrorResponse 객체를 ResponseEntity로 래핑해서 리턴한 반면 위 코드에서는 ResponseEntity가 사라지고 ErrorResponse 객체를 바로 리턴하고 있습니다.
그리고 @ResponseStatus 애너테이션을 이용해서 HTTP Status를 HTTP Response에 포함하고 있습니다.
@RestControllerAdvice = @ControllerAdvice + @ResponseBody
@RestControllerAdvice 애너테이션은 @ControllerAdvice의 기능을 포함하고 있으며, @ResponseBody의 기능 역시 포함하고 있기 때문에 JSON 형식의 데이터를 Response Body로 전송하기 위해서 ResponseEntity로 데이터를 래핑 할 필요가 없다는 사실을 기억해두길 바랍니다.