노션으로 다시 돌아갔습니다 😅

[예외 처리] @RestControllerAdvice 사용한 예외 처리

by mignon25

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

  • 클래스 레벨에 @RestControllerAdvice를 추가하고 해당 클래스에 예외 처리 관련 메서드 작성
  • 이 애너테이션이 적용되어 있으면 모든 Controller 클래스에서 @ExceptionHandler, @InitBinder, @ModelAttribute 가 추가된 메서드를 공유해서 사용할 수 있다. 

👉🏻  예외 처리 메서드의 중복을 제거하고 예외 처리를 공통화할 수 있다. 

 

@InitBinder@ModelAttribute

 

1. MemberController에서 @ExceptionHandler 로직 제거

 

2. ExceptionAdvice 클래스 정의

@RestControllerAdvice
public class GlobalExceptionAdvice {

}

@RestControllerAdvice 를 적용했으므로, Controller 클래스에서 발생하는 예외를 GlobalExceptionAdvice 가 도맡아서 처리하게 된다. 

 

 

3. Exception 핸들러 메서드 구현

  • 이전에 Controller에 직접 적용했던 @ExceptionHander 메서드들을 옮겨놓고,
  • 클래스 레벨에 @RestControllerAdvice를 추가한 것 
@RestControllerAdvice
public class GlobalExceptionAdvice {

    @ExceptionHandler
    public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {

        final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();

        final 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);
    }

    @ExceptionHandler
    public ResponseEntity handleConstraintValidationException(ConstraintViolationException e) {
        // TODO should implement for validation

        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }

}

=> RequestBody의 유효성 검증에 대한 에러는 @RestControllerAdvice 를 통해 모든 어드바이스에 공통 적용

 

4. ErrorResponse 수정

URI 변수로 넘어오는 값의 유효성 검증에 대한 에러(ConstraintViolationException) 처리 구현 목적
@Getter
public class ErrorResponse {

    private List<FieldError> fieldErrors; // 1
    private List<ConstraintViolationError> violationErrors; // 2

    // 3. private 생성자
    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());
        }

    } // Static Inner Class FieldError


    // 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());
        }

    } // Static Inner Class ConstraintViolationError

} // ErrorResponse

< 수정 사항 >

ConstraintViolationException에 대한 Error Response까지 생성 가능하도록 수정

 

  1.  MethodArgumentNotValidException 으로부터 발생하는 에러 정보를 담는 멤버 변수
    => DTO 멤버 변수 필드의 유효성 검증 실패로 발생한 에러 정보를 담는 멤버 변수
     
  2. ConstraintViolationException 으로부터 발생하는 에러 정보를 담는 멤버 변수
    => URI 변수 값의 유효성 검증 실패로 발생한 에러 정보를 담는 멤버 변수
     
  3. ErrorResponse 클래스의 생성자에 private 접근 제한자(Access Modifier) 지정
    => 외부에서 new 로 ErrorResponse 객체 생성 불가
    => 대신 4, 5 처럼 of() 메서드를 이용해 ErrorResponse 객체 생성
    => ErrorResponse 객체 생성과 동시에 ErrorResponse의 역할을 명확하게 해준다. 
       
  4. MethodArgumentNotValidException 에 대한 ErrorResponse 객체 생성
    => MethodArgumentNotValidException 에서 에러 정보를 얻기 위해 필요한 객체 :  BindingResult 
    => of() 메서드를 호출하는 쪽에서 BindingResult 객체를 파라미터로 넘겨준다. 
    => BindingResult에서 필요한 에러 정보만 추출하고 가공하는 일은 ErrorResponse 클래스의 static 멤버 클래스인 FieldError 클래스에게 위임
     
  5. ConstraintViolationException 에 대한 ErrorResponse 객체 생성
    => ConstraintViolationException 에서 에러 정보를 얻기 위해 필요한 객체 : Set<ConstraintViolation<?>>
    => of() 메서드를 호출하는 쪽에서 Set<ConstraintViolation<?>> 객체를 파라미터로 넘겨준다.
    => 4와 마찬가지로, Set<ConstraintViolation<?>> 에서 필요한 에러 정보만 추출하고 가공하는 일은 ErrorResponse 클래스의 static 멤버 클래스인 ConstraintViolationError 클래스에게 위임
       
  6. 필드(DTO 클래스의 멤버 변수)의 유효성 검증에서 발생하는 에러 정보 생성
    => 필요한 정보만 추출, 가공
        
  7. URI 변수 값에 대한 에러 정보 생성
    => 필요한 정보만 추출, 가공
     
에러 유형에 따른 에러 정보 생성 역할을 분리함으로써, ErrorResponse를 사용하는 입장에서는 한층 더 사용하기 편리해졌다. 

 

  • of 메서드
더보기

of() 메서드
of() 메서드는 Java 8의 API에서도 흔히 볼 수 있는 네이밍 컨벤션(Naming Convention)입니다.
주로 객체 생성시 어떤 값들의(of~) 객체를 생성한다는 의미에서 of() 메서드를 사용한다는 점을 기억하면 좋을 것 같습니다.

 

 

5. Exception 핸들러 메서드 수정

수정된 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 handleConstraintValidationException(ConstraintViolationException e) {

        final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());

        return response;
    }

}

< 변경 사항 >

  • Error Response 정보를 만드는 역할을 ErrorResponse 클래스가 대신해주고 있어 코드 자체가 무척 간결해졌다.
  • 기존에는 ErrorResponse 객체를 ResponseEntity로 래핑해서 리턴
  • 현재는 ErrorResponse 객체 바로 리턴
  • @ResponseStatus 애너테이션을 이용해 HTTP Status를 HTTP Response에 포함시킴

 

@RestControllerAdvice VS @ControllerAdvice

@RestControllerAdvice  =  @ControllerAdvice  + @ResponseBody
  • Spring MVC 4.3 버전 이후부터 @RestControllerAdvice 애너테이션 지원
  • @ResponseBody 기능도 포함하고 있기 때문에, JSON 형식 데이터를 Response Body로 전송하기 위해 ResponseEntity로 데이터를 래핑할 필요가 없다. 

 

 

Additional Keywords

 

 

 

블로그의 정보

Mignon'S Dev Log

mignon25

활동하기