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

Lundy·2023년 11월 18일
0
post-thumbnail

기존의 예외처리 방식(Controller 내부에 @ExceptionHandler 를 사용하여 예외처리)을 사용했을 때, Controller 클래스마다 중복이 다수 발생하는 것을 발견했다.

또한 Controller의 예외 발생에도 다양한 종류가 있기 때문에 에러 처리 코드 역시 증가하게 된다.

이러한 문제를 개선하기 위해 @RestControllerAdvice 를 사용해서 예외처리를 공통화 하는 방법을 알아보자.

1. ExceptionAdvice 클래스 생성

import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionAdvice {

}
  • @RestControllerAdvice
    • 모든 Controller 에서 발생하는 예외를 처리함

2. ErrorResponse 클래스 생성

@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() 메서드
    • 네이밍 컨벤션
    • 객체 생성 시 어떤 값들의 객체를 생성한다는 의미에서 사용

3. 핸들러 메서드 구현

@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에 포함함

4. Controller 수정

  • Controller 내부에 @ExceptionHandler 를 붙여 구현되었던 예외처리 메서드를 삭제하고, Controller에는 Controller가 해야할 로직만 구성하면 된다.

🧩 예외처리 추가하는 방법


한 발자국 더 나아가서,

위 예시에서 사용했던 MethodArgumentNotValidException (DTO 클래스에서 검증하는 멤버 변수의 유효성 검증에 실패하는 경우) 뿐만 아니라 다른 예외의 경우도 처리할 수 있도록 추가하는 방법에 대해 알아보자.

추가될 예외처리는 ConstraintViolationException (URI 변수로 넘어오는 값의 유효성 검증에 실패하는 경우) 에러에 대한 처리이다.

1. ErrorResponse 클래스 수정

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

2. Exception 핸들러 메서드 수정

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

💻 예외처리 결과 (with Postman)

  • MethodArgumentNotValidException 을 발생시킨 경우

  • ConstraintViolationException 을 발생시킨 경우

profile
아주 사소하더라도

0개의 댓글

관련 채용 정보