Spring 예외 처리

귀찮Lee·2022년 6월 28일
0

Spring

목록 보기
20/30
post-custom-banner

◎ 예외 처리 필요성

  • 예외 처리를 기본적으로 하지 않는다면, HTTP 응답 내용은 다음과 같다. (유효성 검사 통과 실패 기준)
{
  "timestamp" : "2022-06-28T15:28:32.132+00:00",
  "status" : 400,
  "error" : "Bad Request",
  "path" : "v1/members"
}
  • RequestBody만을 보고는 어떤 데이터가 잘못되었는지 알 수가 없으므로, 클라이언트 쪽에세 에러메세지를 구체적으로 알 수 있도록 바꾸는 작업이 필요함

◎ Spring MVC 예외 처리

  • @ExceptionHandler

    • 한 Controller 클래스 내에서 발생하는 예외를 처리할 수 있다.
    • 이 방법을 모든 곳에 적용한다면, Controller마다 동일하게 발생하는 예외 처리에 대한 중복 코드가 발생할 수 있다.
    • @ExceptionHandler 만으로는 다양한 유형의 예외를 처리하기에는 힘들다.
    @RestController
    @RequestMapping("/v6/members")
    @Validated
    @Slf4j
    public class MemberController {
        ...
        ...
    
        @ExceptionHandler
        // 인자로 받는 예외가 발생할 때, 아래 코드를 실행
        public ResponseEntity handleException(MethodArgumentNotValidException e) {
    
            // e.getBindingResult().getFieldErrors() 에 에러 내용을 담고 있음
            final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
    
            // ResponseEntity를 통해 return, 관련 정보를 모두 담고 있음
            return new ResponseEntity<>(fieldErrors, HttpStatus.BAD_REQUEST);
        }
    
        @ExceptionHandler
        public ResponseEntity handleConstraintViolationException(
                ConstraintViolationException e) {
    
            final List<ConstraintViolation> constraintViolations = e.getConstraintViolations();
            return new ResponseEntity<>(constraintViolations, HttpStatus.BAD_REQUEST);
        }
    }
  • @RestControllerAdvice

    • @RestControllerAdvice 애너테이션을 추가한 클래스를 이용하면 예외 처리를 공통화 가능
    • @RestControllerAdvice 애너테이션을 추가시, Controller 클래스에서 발생하는 예외를 도맡아서 처리
    @RestControllerAdvice
    public class GlobalExceptionAdvice {
        
        @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);
        }

◎ ErrorResponse Object

  • 현 상황의 문제점

    • 필요없는 정보까지 전부 전달하고 있음
    • 이 과정을 ExceptionHandler에서 처리하기에는 코드가 길어지고, SRP에서도 어긋남
  • ErrorResponse Object

    • 따로 객체를 만들어 필드에 지정된 특정 값들만 메세지를 보냄
    • fieldErrors 등등의 에러의 원본 정보를 특정 정보로 다듬는 과정도 이 객체에 같이 담음
    @Getter
    public class ErrorResponse {
        private List<FieldError> fieldErrors;
        private List<ConstraintViolationError> violationErrors;
    
        private ErrorResponse(final List<FieldError> fieldErrors,
                              final List<ConstraintViolationError> violationErrors) {
            this.fieldErrors = fieldErrors;
            this.violationErrors = violationErrors;
        }
    
            // BindingResult에 대한 ErrorResponse 객체 생성
        public static ErrorResponse of(BindingResult bindingResult) {
            return new ErrorResponse(FieldError.of(bindingResult), null);
        }
    
            // Set<ConstraintViolation<?>> 객체에 대한 ErrorResponse 객체 생성
        public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
            return new ErrorResponse(null, ConstraintViolationError.of(violations));
        }
    
        //Field Error 가공
        @Getter
        @AllArgsConstructor
        public static class FieldError {
            private String field;
            private Object rejectedValue;
            private String 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
        @AllArgsConstructor
        public static class ConstraintViolationError {
            private String propertyPath;
            private Object rejectedValue;
            private String 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());
            }
        }
    }
    @RestControllerAdvice
    public class GlobalExceptionAdvice {
    
        @ExceptionHandler
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public ErrorResponse handleMethodArgumentNotValidException(
                MethodArgumentNotValidException e){
            final ErrorResponse response = ErrorResponse.of(e.getBindingResult());
    
            return response;
        }
    
        ...
    
    }

◎ 비즈니스 로직 예외처리

  • 체크 예외와 언체크 예외

    • 체크 예외(Checked Exception) : 발생한 예외를 잡아서(catch) 체크한 후에 어떤 구체적인 처리를 해야 하는 예외 (try, catch 문으로 처리해주어야 함)

      • ex) ClassNotFoundException, SQLException
    • 언체크 예외(Unchecked Exception):예외를 잡아서(catch) 해당 예외에 대한 어떤 처리를 할 필요가 없는 예외

      • RuntimeException을 상속함
      • ex) NullPointerException, ArrayIndexOutOfBoundsException
    • RuntimeException을 상속하여 직접 언체크 예외를 만들 수 있다.

  • 개발자가 의도적으로 예외를 던지는 상황

    • 외부 시스템과의 연동에서 발생하는 에러
      • 외부 API를 이용하는 부분에서 외부 API의 응답이 없을 때
    • 조회하려는 리소스(자원, Resource)가 없는 경우
      • 특정 데이터를 PK를 이용해서 조회할 때, 해당 PK에 데이터가 없을 때
  • 사용자 정의 예외(Custom Exception)

    • 기존에 있는 예외를 이용해서 예외를 던질 수 있으나, 조금더 구체적으로 예외 상황을 표현하기 위해 사용자 정의 예외를 만들 수 있다.
    • RuntimeException을 상속받아 예외를 구현할 수 있다.
    public enum ExceptionCode {
        MEMBER_NOT_FOUND(404, "Member Not Found");
    
        @Getter
        private int status;
    
        @Getter
        private String message;
    
        ExceptionCode(int status, String message) {
            this.status = status;
            this.message = message;
        }
    }
    public class BusinessLogicException extends RuntimeException {
        @Getter
        private ExceptionCode exceptionCode;
    
        public BusinessLogicException(ExceptionCode exceptionCode) {
            super(exceptionCode.getMessage());
            this.exceptionCode = exceptionCode;
        }
    }
    // 실사용 예시
        public Member findMember(long memberId) {
            ...
            throw new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND);
        }
  • 사용자 정의 예외 처리

    • 예외 처리도 @RestControllerAdvice 가 있는 클래스에서 처리하기 용이함
    • ErrorResponse Object 를 이용하여 객체를 통해 에러 정보를 쉽게 전달할 수 있다.
    @RestControllerAdvice
    public class GlobalExceptionAdvice {
        ...
            ...
    
        @ExceptionHandler
        public ResponseEntity handleBusinessLogicException(BusinessLogicException e) {
    
            // ErrorResponse 도 형태를 맞추어 일부 추가해주어야 함
            final ErrorResponse response = ErrorResponse.of(e.getExceptionCode());
            HttpStatus httpStatus = HttpStatus.valueOf(e.getExceptionCode().getStatus())
    
            // 처리마다 에러코드가 다르다면, ResponseEntity를 사용하는 것이 용이
            return new ResponseEntity<>(response, statusCode);
        }
    }
profile
배운 것은 기록하자! / 오류 지적은 언제나 환영!
post-custom-banner

0개의 댓글