기존의 예외처리 방식(Controller 내부에 @ExceptionHandler
를 사용하여 예외처리)을 사용했을 때, Controller 클래스마다 중복이 다수 발생하는 것을 발견했다.
또한 Controller의 예외 발생에도 다양한 종류가 있기 때문에 에러 처리 코드 역시 증가하게 된다.
이러한 문제를 개선하기 위해 @RestControllerAdvice
를 사용해서 예외처리를 공통화 하는 방법을 알아보자.
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionAdvice {
}
@RestControllerAdvice
@Getter
public class ErrorResponse {
/*
DTO 클래스에서 검증하는 멤버 변수의 유효성 검증에 실패하는 경우
(MethodArgumentNotValidException)
해당 변수들의 유효성 검증 실패 에러가 여러가지일 수 있으므로 List 객체 사용
*/
private List<FieldError> fieldErrors;
// private 접근 제한자로 지정 -> of() 메서드를 이용하여 ErrorResponse를 생성하도록
private ErrorResponse(List<FieldError> fieldErrors) {
this.fieldErrors = fieldErrors;
}
/*
BindingResult에 대한 ErrorResponse 객체 생성
BindingResult 객체를 가지고 에러 정보를 추출하고 가공하는 일은
FeldError 클래스에 위임함
*/
public static ErrorResponse of(BindingResult bindingResult) {
return new ErrorResponse(FieldError.of(bindingResult));
}
// 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());
}
}
}
of()
메서드@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
final ErrorResponse response = ErrorResponse.of(e.getBindingResult());
return response;
}
}
ResponseEntity
로 래핑하지 않고 ErrorResponse
객체를 바로 리턴@ResponseStatus
를 사용하여 HTTP Status를 HTTP Response에 포함함@ExceptionHandler
를 붙여 구현되었던 예외처리 메서드를 삭제하고, Controller에는 Controller가 해야할 로직만 구성하면 된다.한 발자국 더 나아가서,
위 예시에서 사용했던 MethodArgumentNotValidException
(DTO 클래스에서 검증하는 멤버 변수의 유효성 검증에 실패하는 경우) 뿐만 아니라 다른 예외의 경우도 처리할 수 있도록 추가하는 방법에 대해 알아보자.
추가될 예외처리는 ConstraintViolationException
(URI 변수로 넘어오는 값의 유효성 검증에 실패하는 경우) 에러에 대한 처리이다.
@Getter
public class ErrorResponse {
private List<FieldError> fieldErrors;
private List<ConstraintViolationError> violationErrors; // 필드 추가
// DI 수정
private ErrorResponse(List<FieldError> fieldErrors, List<ConstraintViolationError> violationErrors) {
this.fieldErrors = fieldErrors;
this.violationErrors = violationErrors;
}
public static ErrorResponse of(BindingResult bindingResult) {
// ConstraintViolationError는 null처리
return new ErrorResponse(FieldError.of(bindingResult), null);
}
// of() 메서드 추가
// Set<ConstraintViolation<?>> 객체에 대한 ErrorResponse 객체 생성
public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
return new ErrorResponse(null, ConstraintViolationError.of(violations));
}
@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());
}
}
// 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());
}
}
}
@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;
}
}
MethodArgumentNotValidException
을 발생시킨 경우ConstraintViolationException
을 발생시킨 경우