예외 처리 - RestControllerAdvice

kwak woojong·2022년 6월 28일
0

코드스테이츠

목록 보기
27/36
post-thumbnail

리액트는 CSR(클라이언트 사이드 랜더링)임.

그래서 제목에 저 어노테이션이 필요하다.

나는 타임리프 밖에(SSR) 안다뤘었기 때문에 @ModelAttribute랑 BindingResult를 통해 예외처리를 했었음.

근데 리액트는 CSR이라 저 @ModelAttribute를 사용하기가 어렵다.

그래서 RestControllerAdvice를 통해 @ExceptionHandler를 사용해야 한다.


@RestControllerAdvice

@ExceptionHandler, @InitBinder, @ModelAttribute가 적용된 메서드에 AOP를 활용하여 Controller 단에 적용하게끔 만들어주는 어노테이션이다.

얘도 애초에 @Component 처리가 되어 있어서 자동으로 빈 등록이 된다.

ResponseBody도 붙어있기 때문에 예외처리가 된다면 자동으로 응답 객체를 리턴해준다.

즉 전체 컨트롤단 (패키지를 지정해둔다면 일부 컨트롤)에 AOP를 적용시킬 수 있다는 뜻이다.

AOP를 적용시키려면 새로운 클래스가 필요하고 거기 안에 로직을 넣으면 되는거다.


ExceptionHandle 적용

@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;
    }
 }

GlobalExceptionAdvice라는 클래스를 생성한 다음에 @RestControllAdvice를 적용했다.

이제 이 안에 있는 메서드는 AOP를 활용해 컨트롤단에 적용될 것이다.

우선 핸들러에 잘못된 문구나 값이 들어올 경우 처리하는 메서드다.

@ResponseStatus로 이 메서드가 작동할 경우 Bad_request를 뱉도록 만들었다.

ErrorResponse라는 객체를 뱉을 건데, 얘가 MethodArgumentNotValidException를 가지면 에러 메시지를 binding 해서 저장하고 return 한다. 그 아래노 마찬가지! 즉 우리가 생각한 예외가 터지면 얘들이 다 잡아줄 수 있다.

만약 통짜로 잡고 싶으면 걍 Exception e 를 인자로 넣으면 되긴 하는데... 그렇게 쓸 일은 없을 것 같다.

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

ErrorResponse는 이렇게 생겼다.

필드 에러가 여러개일 수 있으니 List인 애 하나
violationError은 ConstraintViolationError가 잡힐 때 쓰는 List다.

생성자를 private로 잡아놓고, 실제 객체 생성은 of라는 메서드로 빼게 했다.

private 생성자?

1. 불필요한 객체 생성을 제한한다.

보통 객체를 사용할 땐, 인스턴스를 생성하고 활용한다.
모든 메서드나 클래스가 static인 경우 굳이 인스턴스 생산이 필요가 없다.
어차피 그냥 쓸 수 있으니까!
그러므로 불필요한 생성자를 통해 인스턴스를 만들기보다. public static으로 다른 메서드를 통해
불필요한 객체 생성을 줄일 수 있다.

2. 인자값을 명확하게 넣어줌으로써 각각의 목적을 명확하게 한다.

상기 코드에서 ErrorResponse를 뱉는 of 메서드는 총 2개다.
FieldError가 있을 경우 violationErrors 값은 무적권 null을 뱉는다.
그 반대의 경우도 마찬가지
즉 이 경우엔 두 인자가 모두 필요하긴 하지만 같이 생산할 필요성이 없기 때문에
생성자를 private로 돌리고 다른 메서드를 통해 각기 다른 인자를 가지도록 메서드를 만든 것이다.
public 생성자로 만들 방법도 어찌저찌 있을 수는 있지만, 작업이 매우 복잡해질 가능성이 높다.
이에 그냥 클래스 내부에서만 생성자를 호출해서 작업을 위임한다.
즉 인자값을 명확하게 해주기 위해 생성자 호출을 내부에서만 돌리는 모양이 된 것이다.

@Getter
    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(error -> new FieldError(
                            error.getField(),
                            error.getRejectedValue() == null ?
                                    "" : error.getRejectedValue().toString(),
                            error.getDefaultMessage()))
                    .collect(Collectors.toList());
        }
    }

    @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의 내부 클래스인 FieldError과 ConstraintViolationError이다.
이 두 클래스 역시 private 생성자를 사용하고 있다. 이 경우 static이므로 불필요한 인스턴스 생성을 막은 것으로 볼 수 있겠다.

두 클래스 모두 상수로 3가지를 가지고 있는데, 각각 에러를 발생한 부분, 거부된 값, 그 이유 에 대해 수집하고 있는 모양세다.

이는 of 메서드를 보면 확연히 할 수 있다.


DTO에 예외 어노테이션 처리를 잘 해놨거나, 클래스를 사용해서 예외 처리를 잘 해둔다면, 상기 코드로 앵간하면 다 잡을 수 있다. 여기서 메시지 부분은 또 다른 이야기인데, 다양한 방법으로 커스텀한 메시지를 보낼 수 있다.

이를테면 커스텀 에러를 만들고 Enum을 상황에 따라 넣는 방식도 있을 수 있겠다.

내가 경험했던 Tymeleaf의 경우 (@ModelAttribute와 bindingResult를 씀)

메시지 파일을 따로 만들어서 (마치 application.properties처럼) 넣어주면 알아서 국제화 + 메시지 커스텀화를 다 해줬었다.

메시지 문제는 조금 더 공부해봐야 하겠다.

profile
https://crazyleader.notion.site/Crazykwak-36c7ffc9d32e4e83b325da26ed8d1728?pvs=4<-- 포트폴리오

0개의 댓글