Spring MVC에서의 예외처리(2) - @RestControllerAdvice

Backend kwon·2023년 8월 18일
0

@RestControllerAdvice를 사용한 예외 처리 공통화

특정 클래스에 @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로 데이터를 래핑 할 필요가 없다는 사실을 기억해두길 바랍니다.

profile
백엔드개발자를 향해서

0개의 댓글