아무거나

[Spring Boot] @ControllerAdvice 를 사용한 Global Exception Handler 처리 본문

Java & Kotlin/Spring

[Spring Boot] @ControllerAdvice 를 사용한 Global Exception Handler 처리

전봉근 2022. 7. 26. 15:15
반응형

Java + Spring Boot 개발시에 빈번하게 작성되는 try ~ catch 또는 예외처리등을 간소화하며 에러를 공통으로 핸들링하기 위해 전역으로 사용되는 Exception Handler 객체를 만들어보자. 

@ControllerAdvice 사용한 방법으로 진행하자.

 

자세한 코드는 https://github.com/bkjeon1614/java-example-code/tree/develop/bkjeon-mybatis-codebase 를 참고하면 된다.

 

Global Exception Handler 처리

  • 에러 객체 생성
    [ErrorResponse.java]
    import java.util.ArrayList;
    import java.util.List;
    import java.util.stream.Collectors;
    
    import org.springframework.validation.BindingResult;
    import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
    
    import com.example.bkjeon.enums.exception.ErrorCode;
    
    import lombok.AccessLevel;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    
    /**
    * message: 에러 메세지
    * status: http status code
    * errors: 요청 값에 대한 field, value, reason 작성, 일반적으로 @Valid 로 검증을 진행
    * code: 에러 코드 값
    */
    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public class ErrorResponse {
    
        private String message;
        private int status;
        private List<FieldError> errors;
        private String code;
    
        private ErrorResponse(final ErrorCode code, final List<FieldError> errors) {
            this.message = code.getMessage();
            this.status = code.getStatus();
            this.errors = errors;
            this.code = code.getCode();
        }
    
        private ErrorResponse(final ErrorCode code) {
            this.message = code.getMessage();
            this.status = code.getStatus();
            this.code = code.getCode();
            this.errors = new ArrayList<>();
        }
    
        public static ErrorResponse of(final ErrorCode code, final BindingResult bindingResult) {
            return new ErrorResponse(code, FieldError.of(bindingResult));
        }
    
        public static ErrorResponse of(final ErrorCode code) {
            return new ErrorResponse(code);
        }
    
        public static ErrorResponse of(final ErrorCode code, final List<FieldError> errors) {
            return new ErrorResponse(code, errors);
        }
    
        public static ErrorResponse of(MethodArgumentTypeMismatchException e) {
            final String value = e.getValue() == null ? "" : e.getValue().toString();
            final List<ErrorResponse.FieldError> errors = ErrorResponse.FieldError.of(e.getName(), value, e.getErrorCode());
            return new ErrorResponse(ErrorCode.INVALID_INPUT_VALUE_BINDING_ERROR, errors);
        }
    
        @Getter
        @NoArgsConstructor(access = AccessLevel.PROTECTED)
        public static class FieldError {
            private String field;
            private String value;
            private String reason;
    
            private FieldError(final String field, final String value, final String reason) {
                this.field = field;
                this.value = value;
                this.reason = reason;
            }
    
            public static List<FieldError> of(final String field, final String value, final String reason) {
                List<FieldError> fieldErrors = new ArrayList<>();
                fieldErrors.add(new FieldError(field, value, reason));
                return fieldErrors;
            }
    
            private static List<FieldError> of(final 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());
            }
        }
    
    }  
    
  • 에러 코드 정의
    [ErrorCode.java]
    import org.springframework.http.HttpStatus;
    
    import com.fasterxml.jackson.annotation.JsonFormat;
    
    import lombok.Getter;
    
    /**
    * 에러 코드 정의
    * > 코드값은 _ 앞의 첫자리 대문자를 따서 코드값 + Http Status Code 로 합쳐서 만듬
    * > 에러 메세지는 Common 과 각 도메인별로 관리
    */
    @Getter
    @JsonFormat(shape = JsonFormat.Shape.OBJECT)
    public enum ErrorCode {
    
      // Common
      BAD_REQUEST(HttpStatus.BAD_REQUEST.value(), "ECBR400", "Bad Request"),
      METHOD_ARGUMENT_TYPE_ENUM_BINDING_MISMATCH(HttpStatus.BAD_REQUEST.value(), "ECEBM400", "Bad Request (Method Argument Type Mismatch Error)"),
      UNAUTHORIZED(HttpStatus.UNAUTHORIZED.value(), "ECU401", "Unauthorized"),
      FORBIDDEN(HttpStatus.FORBIDDEN.value(), "ECF403", "Forbidden"),
      NOT_FOUND(HttpStatus.NOT_FOUND.value(), "ECN404", "Not Found"),
      METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED.value(), "ECMNA405", "Method Not Allowed"),
      CONFLICT(HttpStatus.CONFLICT.value(), "ECC409", "Conflict"),
      PRECONDITION_FAILED(HttpStatus.PRECONDITION_FAILED.value(), "ECPF412", "Precondition Failed"),
      TOO_MANY_REQUESTS(HttpStatus.TOO_MANY_REQUESTS.value(), "ECTMR429", "Too Many Requests"),
      INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR.value(), "ECISE500", "Internal Server Error"),
      BAD_GATEWAY(HttpStatus.BAD_GATEWAY.value(), "ECBG502", "Bad Gateway"),
      SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE.value(), "ECSU503", "Service Unavailable"),
      INVALID_INPUT_VALUE_BINDING_ERROR(HttpStatus.BAD_REQUEST.value(), "ECIIVBE400", "Invalid Input Value Binding"),
    
      // Board
      BOARD_INSERT_FAILED(HttpStatus.BAD_REQUEST.value(), "EBBIF001", "Board Data Insert Error"),
      BOARD_UPDATE_FAILED(HttpStatus.BAD_REQUEST.value(), "EBBUF002", "Board Data Update Error"),
      BOARD_EMPTY(HttpStatus.BAD_REQUEST.value(), "EBBE003", "Board Data Empty");
    
      private final String code;
      private final String message;
      private int status;
    
      ErrorCode(final int status, final String code, final String message) {
          this.status = status;
          this.message = message;
          this.code = code;
      }
    
    }    
    
  • 예외 핸들러에 사용할 결과 모델 클래스 생성
    [ApiResponse.java]
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Getter;
    
    @Getter
    @AllArgsConstructor
    @Builder
    public class ApiResponse<T> {
    
        private int statusCode;
        private String responseMessage;
        private T param;
        private T data;
    
        public static<T> ApiResponse<T> res(
            final int statusCode,
            final String responseMessage,
            final T param,
            final T responseData
        ) {
            return ApiResponse.<T>builder()
                .statusCode(statusCode)
                .responseMessage(responseMessage)
                .param(param)
                .data(responseData)
                .build();
        }
    
    }    
    
  • Custom Exception 관련 클래스 생성 (비즈니스 로직 예외 처리 용도)
    [BoardException.java]
    import com.example.bkjeon.enums.exception.ErrorCode;
    
    import lombok.Getter;
    
    @Getter
    public class BoardException extends RuntimeException {
    
      private ErrorCode errorCode;
    
      public BoardException(String message, ErrorCode errorCode) {
        super(message);
        this.errorCode = errorCode;
      }
    
      public BoardException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
      }
    
    }    
    
  • @ControllerAdvice로 모든 예외 핸들링 정의
    [ApiExceptionHandler.java]
    import java.util.HashMap;
    import java.util.Map;
    
    import org.springframework.validation.BindException;
    import org.springframework.validation.FieldError;
    import org.springframework.web.HttpRequestMethodNotSupportedException;
    import org.springframework.web.bind.MethodArgumentNotValidException;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
    
    import com.example.bkjeon.enums.exception.ErrorCode;
    import com.example.bkjeon.model.response.ApiResponse;
    
    import lombok.extern.slf4j.Slf4j;
    
    @ControllerAdvice
    @Slf4j
    public class ApiExceptionHandler {
    
          /**
          * javax.validation.Valid or @Validated 으로 binding error 발생시 발생한다.
          * HttpMessageConverter 에서 등록한 HttpMessageConverter binding 못할경우 발생
          * 주로 @RequestBody, @RequestPart 어노테이션에서 발생
          */
          @ExceptionHandler(MethodArgumentNotValidException.class)
          @ResponseBody
          private ApiResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
            Map<String, String> errorMap = new HashMap<>();
            e.getBindingResult().getAllErrors()
               .forEach(c -> errorMap.put(((FieldError) c).getField(), c.getDefaultMessage()));
    
            StringBuilder sb = new StringBuilder();
            sb.append(ErrorCode.INVALID_INPUT_VALUE_BINDING_ERROR.getMessage());
            sb.append(" (");
            for (Object o: errorMap.entrySet()) {
                sb.append(o);
                break;
            }
            sb.append(")");
    
            log.error("=================== MethodArgumentNotValidException Error !!: {}", e);
    
            return ApiResponse.builder()
                .statusCode(ErrorCode.INVALID_INPUT_VALUE_BINDING_ERROR.getStatus())
                .responseMessage(sb.toString())
                .build();
        }
    
        /**
        * @ModelAttribute 으로 binding error 발생시 BindException 발생한다.
        * ref https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-modelattrib-method-args
        */
        @ExceptionHandler(BindException.class)
        private ApiResponse handleBindException(BindException e) {
            log.error("=================== BindException Error !!", e);
            final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE_BINDING_ERROR, e.getBindingResult());
            return ApiResponse.builder()
                .statusCode(response.getStatus())
                .responseMessage(response.getMessage())
                .build();
        }
    
        /**
        * enum type 일치하지 않아 binding 못할 경우 발생
        * 주로 @RequestParam Enum binding 못했을 경우 발생
        */
        @ExceptionHandler(MethodArgumentTypeMismatchException.class)
        @ResponseBody
        private ApiResponse handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
            log.error("=================== MethodArgumentTypeMismatchException Error !!", e);
            return ApiResponse.builder()
                .statusCode(ErrorCode.METHOD_ARGUMENT_TYPE_ENUM_BINDING_MISMATCH.getStatus())
                .responseMessage(ErrorCode.METHOD_ARGUMENT_TYPE_ENUM_BINDING_MISMATCH.getMessage())
                .build();
        }
    
        /**
        * 지원하지 않은 HTTP method 호출 할 경우 발생
        */
        @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
        @ResponseBody
        private ApiResponse handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
            log.error("=================== HttpRequestMethodNotSupportedException Error !!", e);
            return ApiResponse.builder()
                .statusCode(ErrorCode.METHOD_NOT_ALLOWED.getStatus())
                .responseMessage(ErrorCode.METHOD_NOT_ALLOWED.getMessage())
                .build();
        }
    
        /**
        * 게시물 관련 Custom Exception
        * @param e
        * @return
        */
        @ExceptionHandler(BoardException.class)
        @ResponseBody
        private ApiResponse handleBoardException(BoardException e) {
            log.error("=================== BoardException Error !!", e);
            final ErrorCode errorCode = e.getErrorCode();
            final ErrorResponse response = ErrorResponse.of(errorCode);
    
            return ApiResponse.builder()
                .statusCode(response.getStatus())
                .responseMessage(response.getMessage())
                .build();
        }
    
        @ExceptionHandler(Exception.class)
        @ResponseBody
        private ApiResponse handleException(Exception e) {
            log.error("=================== Exception Error !!", e);
            return ApiResponse.builder()
                .statusCode(ErrorCode.INTERNAL_SERVER_ERROR.getStatus())
                .responseMessage(ErrorCode.INTERNAL_SERVER_ERROR.getMessage())
                .build();
        }
    
    }    
    
  • Exception Test용 컨트롤러 및 파라미터 관련 클래스 생성
    [ApiExceptionController.java]
    import javax.servlet.http.HttpServletRequest;
    
    import org.springframework.http.HttpMethod;
    import org.springframework.validation.BindException;
    import org.springframework.validation.BindingResult;
    import org.springframework.web.HttpRequestMethodNotSupportedException;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.ModelAttribute;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
    
    import com.example.bkjeon.base.exception.error.BoardException;
    import com.example.bkjeon.enums.exception.ErrorCode;
    import com.example.bkjeon.dto.exception.ExceptionDTO;
    
    import io.swagger.annotations.ApiOperation;
    
    @RestController
    @RequestMapping("v1/apiException")
    public class ApiExceptionController {
    
        @ApiOperation("Exception 처리")
        @GetMapping("returnException")
        public void returnException() throws Exception {
            throw new Exception("Exception Error ~!");
        }
    
        @ApiOperation("사용자 에러 처리")
        @GetMapping("customException")
        public void returnCustomException() throws BoardException {
            // Board 관련 예시
            throw new BoardException("setBoardReply Error!", ErrorCode.BOARD_INSERT_FAILED);
        }
    
        @ApiOperation("Method 관련 테스트 이므로 Get 메소드만 성공")
        @RequestMapping("returnHttpRequestMethodNotSupportedException")
        public void returnHttpRequestMethodNotSupportedException(
            HttpServletRequest req) throws HttpRequestMethodNotSupportedException {
    
            if (!HttpMethod.GET.name().equals(req.getMethod())) {
                throw new HttpRequestMethodNotSupportedException("HttpRequestMethodNotSupportedException Error ~!");
            }
        }
    
        @ApiOperation("Enum Type Exception 처리 (ErrorCode ENUM 에 정의되어있는값을 넘겨야 성공 ex: BAD_REQUEST)")
        @GetMapping("handleMethodArgumentTypeMismatchException")
        public void handleMethodArgumentTypeMismatchException(
            @RequestParam("errorCode") ErrorCode errorCode) throws MethodArgumentTypeMismatchException {}
    
    
        @ApiOperation("BindException 처리")
        @PostMapping("returnBindException")
        public void returnBindException(
            @ModelAttribute ExceptionDTO exceptionDTO,
            BindingResult bindingResult
        ) throws BindException {
            throw new BindException(bindingResult);
        }
    
    }    
    
    [ExceptionDTO.java]
    import lombok.AllArgsConstructor;
    import lombok.Getter;
    
    @Getter
    @AllArgsConstructor
    public class ExceptionDTO {
    
        private String title;
        private Integer sort;
    
    }
    

 

 

참고: https://cheese10yun.github.io/

반응형
Comments