1.
@RestControllerAdvice
애너테이션을 추가하면 여러 개의 Controller 클래스에서@ExceptionHandler
,@InitBinder
,@ModelAttribute
가 추가된 메서드를 공유해서 사용할 수 있다.
-> @InitBinder
와 @ModelAttribue
애너테이션은 JSP, Thymeleaf 같은 서버 사이드 렌더링(SSR, Server Side Rendering) 방식에서 주로 사용되는 방식이다.
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
final ErrorResponse response = ErrorResponse.of(e.getBindingResult());
return response;
}
@ExceptionHandler
public ErrorResponse handleConstraintViolationException(
ConstraintViolationException e) {
final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());
return response;
}
}
GlobalExceptionAdvice 클래스에
@RestControllerAdvice
애너테이션을 추가하여 Controller 클래스에서 발생하는 예외를 처리하도록 한다.
Controller에서 유효성 검사 오류가 발생하면 MethodArgumentNotValidException, ConstraintViolationException 이 실행된다.
이 예외들은 GlobalExceptionAdvice에서 잡히고 각각 handleMethodArgumentNotValidException과 handleConstraintViolationException 메서드가 처리한다.
이 메서드들에서는 각각 ErrorResponse.of(BindingResult bindingResult) 와 ErrorResponse.of(Set<ConstraintViolation<?>> violations)를 호출하여 ErrorResponse 객체를 생성한다.
ErrorResponse 객체를 생성할 때, 내부적으로 FieldError.of(BindingResult bindingResult) 또는 ConstraintViolationError.of(Set<ConstraintViolation<?>> violations)를 호출하여 각각의 오류 목록을 만든다.
마지막으로 ErrorResponse 객체가 HTTP 응답으로 반환된다. 클라이언트 측에 어떤 입력 값이 왜 잘못되었는지 알릴 수 있다.
@Getter
public class ErrorResponse {
private List<FieldError> fieldErrors;
private List<ConstraintViolationError> violationErrors;
private ErrorResponse(List<FieldError> fieldErrors, List<ConstraintViolationError> violationErrors) {
this.fieldErrors = fieldErrors;
this.violationErrors = violationErrors;
}
public static ErrorResponse of(BindingResult bindingResult) {
return new ErrorResponse(FieldError.of(bindingResult), null);
}
public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
return new ErrorResponse(null, ConstraintViolationError.of(violations));
}
@Getter // 필드(DTO 클래스의 멤버 변수)의 유효성 검증에서 발생하는 에러 정보를 생성한다.
public static class FieldError {
private String field;
private Object rejectedValue;
private String reason;
public 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(fieldError -> new FieldError(
fieldError.getField(),
fieldError.getRejectedValue() == null ?
"" : fieldError.getRejectedValue().toString(),
fieldError.getDefaultMessage()))
.collect(Collectors.toList());
}
}
@Getter // URI 변수 값에 대한 에러 정보를 생성한다.
public static class ConstraintViolationError {
private String propertyPath;
private Object rejectedValue;
private String reason;
public 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());
}
}
}
private List<FieldError> fieldErrors;
-> MethodArgumentNotValidException 으로 부터 발생하는 에러 정보를 담는 멤버 변수이다.
즉, DTO 멤버 변수 필드의 유효성 검증 실패로 발생한 에러 정보를 담는 멤버 변수이다.
private List<ConstraintViolationError> violationErrors;
-> ConstraintViolationException 으로 부터 발생하는 에러 정보를 담는 멤버 변수이다.
URI 변수 값의 유효성 검증에 실패로 발생한 에러 정보를 담는 멤버 변수이다.
private ErrorResponse(List<FieldError> fieldErrors, List<ConstraintViolationError> violationErrors) {
this.fieldErrors = fieldErrors;
this.violationErrors = violationErrors;
}
-> private 접근 제한자를 사용하여 ErrorResponse 클래스는 new ErrorResponse 방식으로 객체를 생성할 수 없지만,
대신에 of() 메서드를 이용해서 ErrorResponse 객체를 생성할 수 있고 ErrorResponse의 객체를 생성함과 동시에 ErrorResponse의 역할을 명확하게 해준다.
public static ErrorResponse of(BindingResult bindingResult) {
return new ErrorResponse(FieldError.of(bindingResult), null);
}
-> MethodArgumentNotValidException에 대한 ErrorResponse 객체를 생성해 준다.
MethodArgumentNotValidException 에서 에러 정보를 얻기 위해 필요한 것이 바로 BindingResult 객체이므로,
이 of() 메서드를 호출하는 쪽에서 BindingResult 객체를 파라미터로 넘겨주면 된다.
그런데 이 BindingResult 객체를 가지고 에러 정보를 추출하고 가공하는 일은 ErrorResponse 클래스의 static 멤버 클래스인
FieldError 클래스에게 위임하고 있다.
public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
return new ErrorResponse(null, ConstraintViolationError.of(violations));
}
-> ConstraintViolationException에 대한 ErrorResponse 객체를 생성해 준다.
ConstraintViolationException 에서 에러 정보를 얻기 위해 필요한 것이 바로
Set<ConstraintViolation<?>> 객체이므로 이 of() 메서드를 호출하는 쪽에서
Set<ConstraintViolation<?>> 객체를 파라미터로 넘겨주면 된다.
Set<ConstraintViolation<?>> 객체를 가지고 에러 정보를 추출하고 가공하는 일은
ErrorResponse 클래스의 static 멤버 클래스인 ConstraintViolationError 클래스에게
위임하고 있다. (ErrorResponse 객체에 여러 정보를 담는 역할이 명확하게 분리된다.)
of() 메서드는 Java 8의 API에서 볼 수 있는 네이밍 컨벤션(Naming Convention)이다. 주로 객체 생성 시 어떤 값들의 (of~) 객체를 생성한다는 의미에서 of() 메서드를 사용한다.
Set<ConstraintViolation<?>> 에서 <?> 는 Java의 제네릭 타입 ConstraintViolation<?>을 의미하고 '<?>' 는 와일드카드(wildcard)를 나타낸다.
와일드 카드는 어떤 타입이드 가능함을 의미함.
Set<ConstraintViolation<?>> 은 "어떤 타입의 ConstraintViolation 객체든 담을 수 있는 Set" 을 뜻한다.
이렇게 사용하면 ConstraintViolation<String> 이나 ConstraintViolation<Integer> 등 어떤 타입의 ConstraintViolation 객체든 해당 Set에 넣을 수 있다.