@RestControllerAdvice
애너테이션을 추가한 클래스를 이용하면 예외 처리를 공통화 할 수 있다.
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionAdvice {
}
Controller 클래스의 예외를 처리할 GlobalExceptionAdvice
클래스 정의한다. @RestControllerAdvice
애너테이션을 추가하면 이 클래스는 이제 Controller 클래스에서 발생하는 예외를 도맡아서 처리하게 된다.
@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);
}
이제 URI 변수로 넘어오는 값의 유효성 검증에 대한 에러(ConstraintViolationException) 처리에 대한 코드 또한 작성해야한다.
먼저, ErrorResponse
클래스가 ConstraintViolationException
에 대한 Error Response를 생성할 수 있도록 rrorResponse
클래스를 수정해야한다.
@Getter
public class ErrorResponse {
private List<FieldError> fieldErrors; // (1)
private List<ConstraintViolationError> violationErrors; // (2)
// (3)
private ErrorResponse(final List<FieldError> fieldErrors,
final 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());
}
}
}
(1)은 MethodArgumentNotValidException
으로부터 발생하는 에러 정보를 담는 멤버 변수다.
즉, DTO 멤버 변수 필드의 유효성 검증 실패로 발생한 에러 정보를 담는 멤버 변수다.
(2)는 ConstraintViolationException
으로부터 발생하는 에러 정보를 담는 멤버 변수다.
즉, URI 변수 값의 유효성 검증에 실패로 발생한 에러 정보를 담는 멤버 변수다.
(3)은 ErrorResponse 클래스의 생성자이다.
private
접근 제한자를 지정함으로써 new
를 통해 객체를 생성할 수 없지만, (4), (5) 와 같이 of()
메서드를 이용하여 생성할 수 있다.
(4)는 MethodArgumentNotValidException
에 대한 ErrorResponse 객체를 생성해준다. MethodArgumentNotValidException
에서 에러 정보를 얻기 위해 필요한 것이 BindingResult
객체이므로 이 of()
메서드를 호출하는 쪽에서 BindingResult 객체를 파라미터로 넘겨주면 된다.
그런데 이 BindingResult
객체를 가지고 에러 정보를 추출하고 가공하는 일은 ErrorResponse 클래스의 static 멤버 클래스인 FieldError 클래스에게 위임하고 있다.
(5)는 ConstraintViolationException
에 대한 ErrorResponse 객체를 생성해준다.
ConstraintViolationException
에서 에러 정보를 얻기 위해 필요한 것이 바로 Set<ConstraintViolation<?>>
객체이므로 이 of()
메서드를 호출하는 쪽에서 Set<ConstraintViolation<?>>
객체를 파라미터로 넘겨주면 된다.
Set<ConstraintViolation<?>>
객체를 가지고 에러 정보를 추출하고 가공하는 일은 ErrorResponse 클래스의 static 멤버 클래스인 ConstraintViolationError
클래스에게 위임하고 있다.
(4)와 (5)를 통해서 ErrorResponse 객체에 에러 정보를 담는 역할이 명확하게 분리된다.
(6)에서는 필드(DTO 클래스의 멤버 변수)의 유효성 검증에서 발생하는 에러 정보를 생성한다.
(7)에서는 URI 변수 값에 대한 에러 정보를 생성한다.
of() 메서드
of()
메서드는 Java 8의 API에서도 흔히 볼 수 있는 네이밍 컨벤션(Naming Convention)이다.주로 객체 생성시 어떤 값들의(of~) 객체를 생성한다는 의미에서
of()
메서드를 사용한다.
수정된 ErrorResponse 클래스의 메서드를 사용하기 위해 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 vs @ControllerAdvice
Spring MVC 4.3 버전 이후부터 @RestControllerAdvice 애너테이션을 지원하는데, 둘 사이의 차이점을 한마디로 설명하자면 아래와 같다.
• @RestControllerAdvice = @ControllerAdvice + @ResponseBody
@RestControllerAdvice 애너테이션은 @ControllerAdvice 의 기능을 포함하고 있으며, @ResponseBody 의 기능 역시 포함하고 있기 때문에 JSON 형식의 데이터를 Response Body로 전송하기 위해서 ResponseEntity로 데이터를 래핑할 필요가 없다.
@RestControllerAdvice 애너테이션을 추가한 클래스를 이용하면 예외 처리를 공통화 할 수 있다.
@RestControllerAdvice 애너테이션을 사용하면 JSON 형식의 데이터를 Response Body로 전송하기 위해 ResponseEntity로 래핑할 필요가 없다.
@ResponseStatus 애너테이션으로 HTTP Status를 대신 표현할 수 있다.