스프링 에러처리(1) 전역처리와 공통코드 관리 및 커스텀처리
개요
❗에러코드를 구조화하고 싶다.
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.예를들어 @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