스프링에서는 예외를 처리하는 예제를 쉽게 볼 수 있다. 갓갓 우테코 레포에서 많이 참고했다.
이번 글에서는 다양한 예외를 정의하는 것, 구분하는 것, 예외를 사용자에게 어떻게 응답으로 대답해줄지를 다룬다.
## 내부 에러 코드 관리하기
내부 에러코드를 상수로 관리할 필요가 있다. 이 값을 토대로 어떤 예외의 상세 설명을 가능하게 한다.
대충 이렇게만 작성했다.
public enum ErrorCode {
// 404
USER_NOT_FOUND("The user does not exist"),
BALLOON_NOT_FOUND("The balloon does not exist"),
// 500
INTERNAL_SERVER_ERROR("Internal server has a problem"),
// 503
YOUTUBE_API_SERVICE_UNAVAILABLE("The YouTube API service is unavailable"),
SPOTIFY_API_SERVICE_UNAVAILABLE("The Spotify API service is unavailable"),
;
private final String message;
ErrorCode(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
상수로 관리하기 위해 enum으로 만들었고, message는 예외 발생시 로그로 찍어주고 사용자에게 응답으로 던져줄 것이다.
## CommonException
앞서 선언했던 ErrorCode를 사용하는 예외 클래스를 선언한다.
이 클래스는 실질적으로 던지게 되는 예외들에게 인터페이스를 제공한다.
// NestedRuntimeException은 다른 예외를 래핑하는 인터페이스를 제공하는 추상 클래스로, 좀 더 자세한 경위를 알고 싶을때 사용하면 좋다.
// RuntimeException도 생성자에 다른 예외를 받을 수 있게 되어 있어서 꼭 필요한 건 아니다. 그냥 편의상 한다고 봐야할듯.
public abstract class CommonException extends NestedRuntimeException {
private final ErrorCode errorCode;
protected CommonException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
protected CommonException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.errorCode = errorCode;
}
protected CommonException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() {
return errorCode;
}
}
## NotFoundException, ServiceUnavailableException
실제로 던지게 되는 예외들을 정의한다. 기준은 대충 HttpStatus와 비슷하게 만들어 질 것 같다.
이 클래스들은 예외를 큰 범위로 구분하는 역할을 할 것이고, 이후 이들을 ExceptionHandler에서 받아서 따로 처리해줄 것이다.
자바는 public 클래스를 .java 파일당 하나씩만 만들 수 있기 때문에 예외 개수만큼 파일을 만들어줘야 하는데 이건 좀 불편하다.
(깨알 홍보: 파이썬은 부모 생성자를 그대로 가져오기 때문에, 클래스 정의만 만들어도 된다.)
public class NotFoundException extends CommonException {
public NotFoundException(ErrorCode errorCode) {
super(errorCode);
}
}
// 이 예외는 YouTube API나 Spotify API를 사용할 수 없게되면 발생하는 예외인데,
// 이 애플리케이션에서 오류를 다루기 어렵고, 자세한 내용에 대해서 알 필요가 있어 Throwable을 받는다.
public class ServiceUnavailableException extends CommonException {
public ServiceUnavailableException(ErrorCode errorCode) {
super(errorCode);
}
public ServiceUnavailableException(ErrorCode errorCode, Throwable cause) {
super(errorCode, cause);
}
}
// 생성&던지기 할때는 이렇게 해주면 됨!
throw new NotFoundException(ErrorCode.USER_NOT_FOUND);
## GlobalExceptionHandler
예외를 핸들링 하는 클래스를 정의한다.
애플리케이션에서 예외가 발생하면 이 클래스의 메서드가 호출된다.
// @RestControllerAdvice는 @ControllerAdvice + @ResponseBody로 이루어진 어노테이션인데,
// @ControllerAdvice는 예외를 핸들링 하는 클래스라는 것을 의미하고,
// @BesponseBody는 응답을 Json으로 처리하겠다는 의미다.
// 또. ResponseEntityExceptionHandler는 Spring에서 발생하는 기본적인 예외를 핸들링 해준다.
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger log = LoggerFactory.getLogger("ErrorLogger");
private static final String LOG_FORMAT_INFO = "\n[🔵INFO] - ({} {})\n{}\n {}: {}";
private static final String LOG_FORMAT_WARN = "\n[🟠WARN] - ({} {})";
private static final String LOG_FORMAT_ERROR = "\n[🔴ERROR] - ({} {})";
// INFO
/*
[🔵INFO] - (POST /info)
ERROR_CODE_NAME
com.musicinaballoon.common.exception.BadRequestException: 테스트용 에러입니다.
*/
// WARN
/*
[🟠WARN] - (POST /warn)
ERROR_CODE_NAME
com.musicinaballoon.common.exception.InternalServerException: 테스트용 에러입니다.
at com.musicinaballoon.user.UserController.getWarn(UserController.java:129)
*/
// ERROR
/*
[🔴ERROR] - (POST /error)
com.musicinaballoon.common.exception.InternalServerException: 테스트용 에러입니다.
at com.musicinaballoon.user.UserController.getWarn(UserController.java:129)
*/
// @ExceptionHandler 메서드는 어떤 예외를 핸들링 할지 결정한다.
// 모두 함수 시그니쳐가 비슷한데, 로그를 출력하고 아래 정의된 ErrorResponse 객체를 반환한다.
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handle(NotFoundException e, HttpServletRequest request) {
logInfo(e, request);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.from(e));
}
@ExceptionHandler(ServiceUnavailableException.class)
public ResponseEntity<ErrorResponse> handle(ServiceUnavailableException e, HttpServletRequest request) {
logWarn(e, request);
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(ErrorResponse.from(e));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handle(Exception e, HttpServletRequest request) {
logError(e, request);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponse.from(ErrorCode.INTERNAL_SERVER_ERROR));
}
private void logInfo(CommonException e, HttpServletRequest request) {
log.info(LOG_FORMAT_INFO, request.getMethod(), request.getRequestURI(),
e.getErrorCode(), e.getClass().getName(), e.getMessage());
}
private void logWarn(Exception e, HttpServletRequest request) {
log.warn(LOG_FORMAT_WARN, request.getMethod(), request.getRequestURI(), e);
}
private void logError(Exception e, HttpServletRequest request) {
log.error(LOG_FORMAT_ERROR, request.getMethod(), request.getRequestURI(), e);
}
record ErrorResponse(ErrorCode errorCode, String message) {
static ErrorResponse from(CommonException e) {
return new ErrorResponse(e.getErrorCode(), e.getMessage());
}
static ErrorResponse from(ErrorCode errorCode) {
return new ErrorResponse(errorCode, errorCode.getMessage());
}
}
}
'프로그래밍 > A music in a balloon' 카테고리의 다른 글
# 12 프론트 개발 - 지도와 위치 정보 (0) | 2024.06.24 |
---|---|
# 11 바람에 날리는 풍선은 어떻게 구현할까? (2) | 2024.06.11 |
# 9 프로젝트 수정 (0) | 2024.06.06 |
# 8 깃허브 엑션 테스트 적용기 (0) | 2024.06.05 |
# 7 LazyInitializationException을 보았다! (0) | 2024.06.04 |