RestControllerAdvice

seongmin·2022년 10월 25일
0

Spring

목록 보기
24/38
post-thumbnail

@RestControllerAdvice

@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를 대신 표현할 수 있다.

0개의 댓글