아무거나

@ControllerAdvice 를 통한 Global Exception Handler 처리 본문

Java/Spring

@ControllerAdvice 를 통한 Global Exception Handler 처리

전봉근 2022. 8. 2. 17:21
반응형

Exception 을 개선하다가 예전에 포스팅을 작성해야지 하면서 깜박했던 부분이 있어 다시 작성하게되었다.

 

  • @ControllerAdvice 를 통한 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;
      
      }
      

 

반응형
Comments