8 minute read

개요

에러코드를 구조화하고 싶다.

AS-IS

SysException.java

public class SysException extends RuntimeException 
{
	private static final Logger logger = LoggerFactory.getLogger(SysException.class);
	
	//Enum 에러코드 클래스
	private final SysErrorCode errorCode;  
    
    //Constructor1: (커스텀에러세메지만)
    public SysException(SysErrorCode errorCode) {
    	super(errorCode.getMessage());
        this.errorCode = errorCode;
    }

    //Constructor2: (커스텀에러세메지 + 오리지널에러메세지)
    public SysException(SysErrorCode errorCode, Throwable e) {
        super(errorCode.getMessage(), e);
        this.errorCode = errorCode;
        
        logOriginalException(e);
    }

    //[커스텀메세지]와 [오리지날익셉션]내용을 모두 출력.
    private void logOriginalException(Throwable e) {
	    logger.error("[custom Exception SysErrorCode.getMessage()]: {}", errorCode.getMessage()); //커스텀 내용
	    logger.error("[org Exception name]: {}", e.getClass().getName()); //(동일)e.printStackTrace();
	    logger.error("[org Exception message]: {}", e.getMessage()); //오리지날 익셉션 내용
	    logger.error("[org Exception stackTrace]: {}", e.getStackTrace()); //오리지날 익셉션 내용
    }
    
    
    
    public SysErrorCode getErrorCode() {
        return errorCode;
    }
    
}

Exception 에 로그만 찍는 수준
💡 이전에 익셉션이 발생하면 문자열 추가한 로그의 내용을찍는 수준이다.

TO-BE

GlobalExceptionHandler.java : Exception 전역 처리

/*
	 * 1. 모든 컨트롤러에서 발생하는 (@ControllerAdvice 으로 인해. 컴포넌트스캔과 함께 사용되므로 Bean주입이 되어있음.) 
	 * 2. 익셉션들을 @ExceptionHandler(클래스명) 으로 필터링되어 선행처리 된다.
*/
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

	/** javax.validation.Valid or @Validated 으로 binding error 발생시 발생한다. **/
    @ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("handleMethodArgumentNotValidException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }
    /** @ModelAttribut 으로 binding error 발생시 BindException 발생한다. **/
    @ExceptionHandler(BindException.class)
    protected ResponseEntity<ErrorResponse> handleBindException(BindException e) {
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }
	/** enum type 일치하지 않아 binding 못할 경우 발생 **/
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    protected ResponseEntity<ErrorResponse> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
        log.error("handleMethodArgumentTypeMismatchException", e);
        final ErrorResponse response = ErrorResponse.of(e);
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }
	/** 지원하지 않은 HTTP method 호출 할 경우 발생 **/
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    protected ResponseEntity<ErrorResponse> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        log.error("handleHttpRequestMethodNotSupportedException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.METHOD_NOT_ALLOWED);
        return new ResponseEntity<>(response, HttpStatus.METHOD_NOT_ALLOWED);
    }

	/** Authentication 객체가 필요한 권한을 보유하지 않은 경우 발생합 **/
    @ExceptionHandler(AccessDeniedException.class)
    protected ResponseEntity<ErrorResponse> handleAccessDeniedException(AccessDeniedException e) {
        log.error("handleAccessDeniedException", e);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.HANDLE_ACCESS_DENIED);
        return new ResponseEntity<>(response, HttpStatus.valueOf(ErrorCode.HANDLE_ACCESS_DENIED.getStatus()));
    }

    /** [그외 전체 Exception] 모든 Exception 경우 발생 **/
    @ExceptionHandler(Exception.class)
    protected ResponseEntity<ErrorResponse> handleException(Exception ex) {
        //final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR);
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR, ex.getMessage());
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }
    
}

Exception 전역처리
💡 1.모든 컨트롤러에서 발생하는 (@ControllerAdvice 으로 인해. 컴포넌트스캔과 함께 사용되므로 Bean주입이 되어있음.)
💡 2.익셉션들을 @ExceptionHandler(클래스명) 으로 필터링되어 선행처리 된다.

Exception Wrapping 처리하여 응답
💡 직접만든 ErrorResponse 클래스를 생성하여 json포맷으로 wrapping 한다
💡 공통 규약된 에러메세지를 전달 할 수 있다.

1.일부의 Exception 을 감지하도록 처리하고있다.
2.커스텀한 handleCustomException 감지하도록 추가해주었다.
3.그외 전체 Exception.class 자체를 잡아서 감지하도록 추가해주었다.

ErrorResponse.java : Exception 동일한 포맷처리(Wrapping)

/**
 * Error Response
 * 간혹 Map<Key, Value> 형식으로 처리하는데 이는 좋지 않다. 
 * 우선 Map 이라는 친구는 런타입시에 정확한 형태를 갖추기 때문에 객체를 처리하는 개발자들도 정확히 무슨 키에 무슨 데이터가 있는지 확인하기 어렵다.
 * 리턴 타입이 ResponseEntity<ErrorResponse> 으로 무슨 데이터가 어떻게 있는지 명확하게 추론하기 쉽도록 구성하는 게 바람직하다.
 * 
 * 이 클래스의 내용을 POJO 객체로 관리하면 errorResponse.getXXX(); 이렇게 명확하게 객체에 있는 값을 가져올 수 있다. 
 * 그 밖에 특정 Exception에 대해서 ErrorResponse 객체를 어떻게 만들 것인가에 대한 책임을 명확하게 갖는 구조로 설계할 있다.
 */
/**
 * Global Exception Handler에서 발생한 에러에 대한 응답 처리를 관리
 */
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ErrorResponse {

	private int status;                // 에러 상태 코드
	private String code;			   // 에러 구분 코드
    private String message;            // 에러 메시지
    private List<FieldError> errors;   // 상세 에러 메시지
    private String reason;             // 에러 이유 (추가)


    
    /**********************************************************************************************
     * 생성자
     **********************************************************************************************/
    /* ErrorResponse 생성자-1*/
    private ErrorResponse(final ErrorCode code) {
        this.message = code.getMessage();
        this.status = code.getStatus();
        this.code = code.getCode();
        this.errors = new ArrayList<>();
    }
    
    /* ErrorResponse 생성자-2 */
    protected ErrorResponse(final ErrorCode code, final String reason) {
        this.message = code.getMessage();
        this.status = code.getStatus();
        this.code = code.getCode();
        this.reason = reason;
    }
    
    /* ErrorResponse 생성자-3 */
    private ErrorResponse(final ErrorCode code, final List<FieldError> errors) {
        this.message = code.getMessage();
        this.status = code.getStatus();
        this.errors = errors;
        this.code = code.getCode();
    }

    /**********************************************************************************************
     * Global Exception 전송 타입
     **********************************************************************************************/
    /* Global Exception 전송 타입-1 */
    public static ErrorResponse of(final ErrorCode code, final BindingResult bindingResult) {
        return new ErrorResponse(code, FieldError.of(bindingResult));
    }

    /* Global Exception 전송 타입-2 */
    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);
    }

    /* Global Exception 전송 타입-3 */
    public static ErrorResponse of(final ErrorCode code, final String reason) {
        return new ErrorResponse(code, reason);
    }

	/** Global Exception 전송 타입-4 (enum type 일치하지 않아 binding 못할 경우 발생) **/
    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_TYPE_VALUE, errors);
    }
    
    /**********************************************************************************************
     * POJO
	 * 에러를 e.getBindingResult() 형태로 전달 받는 경우 해당 내용을 상세 내용으로 변경하는 기능을 수행한다.
	 * 이 클래스의 내용을 POJO 객체로 관리하면 errorResponse.getXXX(); 이렇게 명확하게 객체에 있는 값을 가져올 수 있다. 
     **********************************************************************************************/
    @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;
        }

		/**
		 * 아래 코드와 동일한 효과
		 * 
		 * List<FieldError> fieldErrorList = Arrays.asList(
   		 * new FieldError("username", "", "Username cannot be empty"),
    	 * new FieldError("age", "-1", "Age must be positive")
		 * );
		 */
        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());
        }
    }
}

Exception Wrapping 처리
💡 1.생성자를 만들고
💡 2.ErrorResponse객체를 생성하는 목적의 여러 메소드를 만든다. (메소드는 생성자를 호출하여 초기화)
💡 3.그 중 BindingResult 형태로 메소드가 호출되면 POJO의 FieldError 기능으로 조립하여 상세한 내용을 리턴한다.

BindingResult는 뭘까
사진1
💡 1.예를들어 @Valid 의 경우에는 MethodArgumentNotValidException 을 발생시킨다.
💡 2.MethodArgumentNotValidException 에는 여러 값들이 있으며, 그 중 BindingResult 라는 객체에 예외에 대한 좀 더 자세하고 많은 정보들이 담겨있다.
💡 3.BindingResult 객체 안에서 error 와 관련된 필요한 값을 가져오기 위해 BindingResult.getFieldErrors() 메소드를 이용하여 BindingResult 가 갖고 있는 errors 를 FieldError 라는 객체 형태로 반환받고
💡 4.FieldError 객체에서 필요한 값들을 사용자(개발자) 가 정의한 예외 객체에 매핑하여 사용한다.

  • Field : 객체에서 예외가 발생한 field
  • RejectedValue : 어떤 값으로 인해 예외가 발생하였는지
  • DefaultMessage : 해당 예외가 발생했을 때 제공할 message 는 무엇인지

1.핵심은 ErrorCode라는 Enum을 받아서 해당 class를 생성하도록 구현한다.
2.Enum을 통해서 한눈에 볼 수 있으며 리턴된 내용의 형식이 일치한다.
3.BindingResult는 Spring Framework에서 폼 태그 검증 결과를 담고 있는 객체로, 주로 e.getBindingResult() 형태로 들어온다.

ErrorCode.java : EnumModel Interface 구현한 공통코드

/**
 * [공통 코드] API 통신에 대한 '에러 코드'를 Enum 형태로 관리를 한다.
 * Global Error CodeList : 전역으로 발생하는 에러코드를 관리한다.
 * Custom Error CodeList : 업무 페이지에서 발생하는 에러코드를 관리한다
 * Error Code Constructor : 에러코드를 직접적으로 사용하기 위한 생성자를 구성한다.
 */
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum ErrorCode {
	
    /**
     * ******************************* Global Error CodeList ***************************************
     * HTTP Status Code
     * 400 : Bad Request
     * 401 : Unauthorized
     * 403 : Forbidden
     * 404 : Not Found
     * 500 : Internal Server Error
     * *********************************************************************************************
     */
    // 잘못된 서버 요청
    BAD_REQUEST_ERROR(400, "G001", "Bad Request Exception"),

    // @RequestBody 데이터 미 존재
    REQUEST_BODY_MISSING_ERROR(400, "G002", "Required request body is missing"),

    // 유효하지 않은 타입
    INVALID_TYPE_VALUE(400, "G003", " Invalid Type Value"),

    // Request Parameter 로 데이터가 전달되지 않을 경우
    MISSING_REQUEST_PARAMETER_ERROR(400, "G004", "Missing Servlet RequestParameter Exception"),

    // 입력/출력 값이 유효하지 않음
    IO_ERROR(400, "G008", "I/O Exception"),

    // com.google.gson JSON 파싱 실패
    JSON_PARSE_ERROR(400, "G009", "JsonParseException"),

    // com.fasterxml.jackson.core Processing Error
    JACKSON_PROCESS_ERROR(400, "G010", "com.fasterxml.jackson.core Exception"),

    // 권한이 없음
    FORBIDDEN_ERROR(403, "G004", "Forbidden Exception"),

    // 서버로 요청한 리소스가 존재하지 않음
    NOT_FOUND_ERROR(404, "G005", "Not Found Exception"),

    // NULL Point Exception 발생
    NULL_POINT_ERROR(404, "G006", "Null Point Exception"),

    // @RequestBody 및 @RequestParam, @PathVariable 값이 유효하지 않음
    NOT_VALID_ERROR(404, "G007", "handle Validation Exception"),

    // @RequestBody 및 @RequestParam, @PathVariable 값이 유효하지 않음
    NOT_VALID_HEADER_ERROR(404, "G007", "Header에 데이터가 존재하지 않는 경우 "),

    // 서버가 처리 할 방법을 모르는 경우 발생
    INTERNAL_SERVER_ERROR(500, "G999", "Internal Server Error Exception"),

    // 서버가 처리 할 방법을 모르는 경우 발생
    //NOT_AVAILABLE_EMAIL_SERVICE(500, "G999", "NHN cloud service is not available."),
    
    AUTH_IS_NULL(200, "A404", "AUTH_IS_NULL"),                // A404
    AUTH_TOKEN_FAIL(200, "A405", "AUTH_TOKEN_FAIL"),            // A405
    AUTH_TOKEN_INVALID(200, "A406", "AUTH_TOKEN_INVALID"),            // A406
    AUTH_TOKEN_NOT_MATCH(200, "A407", "AUTH_TOKEN_NOT_MATCH"),        // A407
    AUTH_TOKEN_IS_NULL(200, "A408", "AUTH_TOKEN_IS_NULL"),        // A408
    

    
	/**
     * ******************************* Custom Error CodeList ***************************************
     */
    // Common
    INVALID_INPUT_VALUE(400, "C001", " Invalid Input Value"),
    METHOD_NOT_ALLOWED(405, "C002", " Invalid Input Value"),
    ENTITY_NOT_FOUND(400, "C003", " Entity Not Found"),
    //INTERNAL_SERVER_ERROR(500, "C004", "Server Error"),
    //INVALID_TYPE_VALUE(400, "C005", " Invalid Type Value"),
    HANDLE_ACCESS_DENIED(403, "C006", "Access is Denied"),

    // Member
    EMAIL_DUPLICATION(400, "M001", "Email is Duplication"),
    LOGIN_INPUT_INVALID(400, "M002", "Login input is invalid"),

    // Coupon
    COUPON_ALREADY_USE(400, "CO001", "Coupon was already used"),
    COUPON_EXPIRE(400, "CO002", "Coupon was already expired"), 
    
    // Business Exception Error
    BUSINESS_EXCEPTION_ERROR(200, "B999", "Business Exception Error"),

    // Transaction Exception Error
    INSERT_ERROR(200, "9999", "Insert Transaction Error Exception"),
    UPDATE_ERROR(200, "9999", "Update Transaction Error Exception"),
    DELETE_ERROR(200, "9999", "Delete Transaction Error Exception"),

    
    
    ;
    /**
     * ******************************* Error Code Constructor ***************************************
     */
	private int status;           // 에러 코드의 '코드 상태'을 반환한다.
    private final String code;    // 에러 코드의 '코드간 구분 값'을 반환한다.
    private final String message; // 에러 코드의 '코드 메시지'을 반환한다.

    //생성자
    ErrorCode(final int status, final String code, final String message) {
        this.status = status;
        this.message = message;
        this.code = code;
    }

    public String getMessage() {
        return this.message;
    }

    public String getCode() {
        return code;
    }

    public int getStatus() {
        return status;
    }

    
}

// 정의한 주요 필드
//   - status (HttpStatus),
//   - code (자체 정의한 ErrorCode),
//   - messsage (Error메시지),

Exception 에러코드 공통 관리
💡 1.각 유형별 에러코드를 Enum 으로 생성
💡 2.Exception이 필요할 때 편하게 던져준다.
💡 3.ExceptionHandler 에서 잡아줄 것이다.

구현 테스트

실행

    @ResponseBody
    @RequestMapping(value = "/app/testBindException", method = RequestMethod.GET)
    public String testBindException(@RequestParam(value = "name", required = false) String name) throws IOException {
        try {
            int errorCALL = tB002Service.setInsert(new TB002());
        } catch (Exception e) {
            throw new IOException();
        }
		return "";
    }
	

서버 결과

# 전역처리된 Exception.class 으로 필터링되었다.

[2024-07-21 19:50:14] DEBUG: org.springframework.jdbc.datasource.DataSourceUtils - Returning JDBC Connection to DataSource
[2024-07-21 19:50:14] ERROR: com.blang.bck.comm.error.GlobalExceptionHandler - Exception
java.io.IOException: null
	at com.blang.bck.web.controller.CmnController.testBindException(CmnController.java:401) ~[classes/:?]
    (중략)
	at java.lang.Thread.run(Thread.java:748) [?:1.8.0_311]
[2024-07-21 19:50:14] DEBUG: com.blang.bck.comm.error.GlobalExceptionHandler - ===========================================================
[2024-07-21 19:50:14] DEBUG: com.blang.bck.comm.error.GlobalExceptionHandler - [handleException()] 여기로 오는가?!
[2024-07-21 19:50:14] DEBUG: com.blang.bck.comm.error.GlobalExceptionHandler - ===========================================================

브라우저 응답

// ResponseError  통해 정형화된 응답을 주고있다. 
{"status":500,"code":"G999","message":"Internal Server Error Exception"}

참고블로그

  • https://cheese10yun.github.io/spring-guide-exception/
  • https://github.com/adjh54ir/spring-security-adjh54/tree/develop-main/src/main/java/com/adjh/sprsecurity/model/common
  • https://devpoong.tistory.com/90
  • https://arinlee.tistory.com/69
  • https://velog.io/@waldo/%EC%97%90%EB%9F%AC%EC%B2%98%EB%A6%AC101-Enum%EC%9C%BC%EB%A1%9C-%EC%97%90%EB%9F%AC%EC%BD%94%EB%93%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0

Leave a comment