[Spring] Custom Exception Handler
Spring에서 exception을 다루는 방식에 대해 다룰 것이다. 총 4개의 파일을 만든다.
디렉토리 구성
exception
- CustomException.java
- ErrorCode.java
- ErrorResponse.java
- GlobalExceptionHandler.java
이렇게 된다.
1. CustomException
RuntimeException을 extend한 것으로, 선언한 ErrorCode를 가지고 있다.
exception/CustomException.java
@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException{
private final ErrorCode errorCode;
}
2. ErrorCode
ErrorCode enumerate는 크게 3가지 정보가 있다. 메시지 이름, http status, 내부에 담을 message. 아래와 같이 error에 대한 이름, http status, message 값을 넣는다.
exception/ErrorCode.java
@Getter
@AllArgsConstructor
public enum ErrorCode{
// 400 BAD_REQUEST
PARAMETER_NOT_VALID(HttpStatus.BAD_REQUEST, "입력 정보가 유효하지 않습니다."),
INVALID_TOKEN(HttpStatus.BAD_REQUEST, "올바르지 않은 토큰입니다."),
INVALID_ACCESS_TOKEN(HttpStatus.BAD_REQUEST, "사용자 정보가 유효하지 않습니다."),
INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "사용자 정보가 유효하지 않습니다."),
// 401 UNAUTHORIZED
MEMBER_STATUS_LOGOUT(HttpStatus.UNAUTHORIZED, "사용자가 로그아웃 상태입니다."),
ID_NOT_EXIST(HttpStatus.UNAUTHORIZED, "아이디가 존재하지 않습니다."),
PASSWORD_NOT_VALID(HttpStatus.UNAUTHORIZED, "비밀번호가 틀렸습니다."),
NO_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "자격 증명에 실패했습니다."),
// 403 FORBIDDEN
NOT_ALLOWED(HttpStatus.FORBIDDEN, "허가되지 않은 접근입니다."),
// 404 NOT_FOUND
PHONE_NUMBER_NOT_EXIST(HttpStatus.NOT_FOUND, "휴대폰 번호가 존재하지 않습니다."),
MEMBER_NOT_EXIST(HttpStatus.NOT_FOUND, "사용자가 존재하지 않습니다."),
// 409 CONFLICT
ID_DUPLICATED(HttpStatus.CONFLICT, "아이디가 중복되었습니다."),
DUPLICATED_RESOURCE(HttpStatus.CONFLICT, "데이터가 이미 존재합니다."),
;
private final HttpStatus httpStatus;
private final String msg;
}
3. ErrorResponse
ErrorResponse class는 에러가 났을 때 리턴해주는 class이다. ErrorCode를 인자로 받아 http status, message 등을 정해진 양식에 맞게 줄 것이다. 나의 경우에는 timestamp, status, error, code, msg 이렇게 5가지 인자를 정했다.
exception/ErrorResponse.java
@Getter
@Builder
public class ErrorResponse {
private final LocalDateTime timestamp = LocalDateTime.now();
private final int status;
private final String error;
private final String code;
private final String msg;
public static ResponseEntity<ErrorResponse> toResponseEntity(ErrorCode errorCode){
return ResponseEntity.status(errorCode.getHttpStatus())
.body(ErrorResponse.builder()
.status(errorCode.getHttpStatus().value())
.error(errorCode.getHttpStatus().name())
.code(errorCode.name())
.msg(errorCode.getMsg())
.build()
);
}
public static JSONObject toJson(ErrorCode errorCode){
JSONObject jsonObject = new JSONObject();
jsonObject.put("timestamp", LocalDateTime.now());
jsonObject.put("status", errorCode.getHttpStatus().value());
jsonObject.put("error", errorCode.getHttpStatus().name());
jsonObject.put("code", errorCode.name());
jsonObject.put("msg", errorCode.getMsg());
return jsonObject;
}
}
4. GlobalExceptionHandler.java
마지막으로 GlobalExceptionHandler는 ResponseEntityExceptionHandler를 extend하며, 발생하는 각종 exception을 다뤄주는 handler이다. 기존에 발생하는 DataIntegrityViolationException이나 SignatureException, 등등 에러를 처리할 수도 있고 내가 넣은 CustomException도 처리해서 ErrorResponse.toResponseEntity를 호출해서 ErrorResponse 형식에 맞추어서 값을 리턴한다.
exception/GlobalExceptionHandler.java
package com.bizkicks.backend.exception;
import java.security.SignatureException;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHander extends ResponseEntityExceptionHandler {
// 입력 정보가 다를 때
// sql에 중복되어 있을 때
@ExceptionHandler(value = {DataIntegrityViolationException.class})
protected ResponseEntity<ErrorResponse> handleIntegrityViolateException(){
log.error("handleDataException throws Exceptions : {}", ErrorCode.PARAMETER_NOT_VALID);
return ErrorResponse.toResponseEntity(ErrorCode.PARAMETER_NOT_VALID);
}
@ExceptionHandler(value = {CustomException.class})
protected ResponseEntity<ErrorResponse> handleCustomException(CustomException e){
log.error("handleDataException throws Exceptions : {}", e.getErrorCode());
return ErrorResponse.toResponseEntity(e.getErrorCode());
}
@ExceptionHandler(value = {SignatureException.class})
protected ResponseEntity<ErrorResponse> handleSignatureException(){
log.error("SignatureException throws Exceptions : {}", ErrorCode.INVALID_ACCESS_TOKEN);
return ErrorResponse.toResponseEntity(ErrorCode.INVALID_ACCESS_TOKEN);
}
}
5. 사용
사용은 customException을 발생시키면 된다.
throw new CustomException(ErrorCode.PARAMETER_NOT_VALID);