애플리케이션 개발 중 예외 처리는 안정성과 가독성을 확보하기 위해 매우 중요한 요소다. Spring Boot에서는 예외를 포괄적으로 관리할 수 있는 @RestControllerAdvice와 GlobalExceptionHandler를 제공하여 효율적인 예외 처리를 할 수 있다. 이 글에서는 GlobalExceptionHandler, ErrorCode, ErrorResponse, 그리고 ApiResponse를 활용하여 예외를 일관되게 처리하고 클라이언트에게 표준화된 응답을 반환하는 방법에 대해 알아보겠다 ! ! !
애플리케이션에서는 다양한 오류가 발생할 수 있다. 예를 들어, 클라이언트의 잘못된 요청, 서버의 비정상적인 상태, 데이터베이스 연관 관계 위반 등의 예외 상황이 있을 수 있다. 이때, 각각의 예외에 대한 응답을 일관성 있게 제공해야만 클라이언트는 예외 상황을 정확히 이해하고 대응할 수 있다 !
예외 처리의 주요 목표:
Spring Boot에서는 @RestControllerAdvice와 @ExceptionHandler를 사용하여 예외를 전역적으로 처리할 수 있다. 이 두 가지 기능을 이용하여 공통의 예외 처리 로직을 구현하고, 클라이언트에게 일관된 형식의 응답을 제공할 수 있게된다.
@RestControllerAdvice: 모든 @RestController에서 발생한 예외를 잡아 처리하는 어노테이션@ExceptionHandler: 특정 예외 클래스에 대해 예외 처리 메서드를 지정하여 처리ErrorCode 클래스는 애플리케이션에서 발생할 수 있는 예외 상황에 대한 코드와 메시지를 정의한다. 각 예외 상황에 대해 고유한 코드와 메시지를 지정하여 일관된 에러 응답을 제공할 수 있다 !
기존에는 세부적인 예외 상황마다 각각의 예외 클래스를 만들어 GlobalExceptionHandler에서 관리했으나, 이로 인해 클래스의 개수가 지나치게 많아지는 문제가 발생했다. 이를 개선하기 위해, 주요 예외 상황에 대해서만 클래스를 생성하고, 세부적인 에러 메시지는 enum 타입으로 관리하는 방식을 도입해보았다. 이러한 접근 방식을 통해 코드의 복잡성을 줄이고, 예외 처리 로직을 보다 간결하게 유지할 수 있었다 !
public enum ErrorCode {
// 400
INVALID_REQUEST_ARGUMENT("잘못된 요청입니다."),
INVALID_ARGUMENT_TYPE("유효하지 않은 타입입니다."),
// 404
MEMBER_NOT_FOUND("해당 회원이 존재하지 않습니다."),
BOOK_NOT_FOUND("해당 도서가 존재하지 않습니다"),
// 500
INTERNAL_SERVER_ERROR("서버 내부에 문제가 발생했습니다."),
FAILED_CREATE_BOOK("도서 저장에 실패해였습니다.");
private final String message;
ErrorCode(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
ErrorResponse 클래스는 발생한 예외에 대한 정보를 클라이언트에게 전달하기 위한 객체이다.
ErrorCode를 기반으로 응답 메시지를 생성하고, HTTP 상태 코드와 메시지를 담아 반환한다.
public class ErrorResponse {
private final String code;
private final String message;
private final int status;
public ErrorResponse(ErrorCode errorCode, int status) {
this.code = errorCode.name();
this.message = errorCode.getMessage();
this.status = status;
}
public static ErrorResponse from(ErrorCode errorCode) {
return new ErrorResponse(errorCode, errorCode.getHttpStatus().value());
}
}
GlobalExceptionHandler는 모든 예외를 포착하고 ErrorCode와 ErrorResponse를 통해 예외를 처리하는 클래스이다. 다양한 예외 클래스에 대해 각각의 처리 메서드를 구현하여 발생한 예외의 원인과 상세 메시지를 클라이언트에게 반환한다.
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handle(NotFoundException exception) {
log.error("NotFoundException: {} | Location: {}", exception.getMessage(), getLocation(exception), exception);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.from(ErrorCode.MEMBER_NOT_FOUND));
}
@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<ErrorResponse> handleUnauthorizedException(UnauthorizedException exception) {
log.error("UnauthorizedException: {} | Location: {}", exception.getMessage(), getLocation(exception), exception);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ErrorResponse.from(ErrorCode.UNAUTHORIZED));
}
}
ApiResponse 클래스는 성공과 실패 응답을 동일한 형식으로 반환하기 위한 객체이다. 이를 통해 성공/실패 여부와 데이터를 쉽고 일관성 있게 클라이언트에게 전달할 수 있다.
public class ApiResponse<T> {
private final boolean success;
private final T data;
private final ErrorResponse error;
private ApiResponse(boolean success, T data, ErrorResponse error) {
this.success = success;
this.data = data;
this.error = error;
} // 이 부분은 마음대로 커스터마이징 하면 된다.
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, data, null);
}
public static ApiResponse<?> error(ErrorResponse error) {
return new ApiResponse<>(false, null, error);
}
}
에러 응답 처리 플로우를 코드를 통해 자세히 설명하려한다. 벨로그는 토글같은 접은글 기능이 없어 아쉽다..
최초 에러 발생
throw new UnauthorizedException(ErrorCode.UNAUTHORIZED_ACCESS);
UnauthorizedException 예외 발생UnauthorizedException 생성 시 ErrorCode.UNAUTHORIZED_ACCESS가 함께 전달되어 예외 객체 내부에 저장ErrorCode 객체에는 HTTP 상태 코드 (HttpStatus.UNAUTHORIZED), 에러 메시지 ("Unauthorized access.") 등의 정보가 포함에러 정보가 ErrorCode 객체에 담김
ErrorCode.UNAUTHORIZED_ACCESS는 다음과 같이 정의UNAUTHORIZED_ACCESS(HttpStatus.UNAUTHORIZED, "Unauthorized access."),
UNAUTHORIZED(401, Series.CLIENT_ERROR, "Unauthorized"),
ErrorCode는 HttpStatus와 에러 메시지를 포함하는 enum 타입의 객체로, 예외 상황을 설명하는 메시지와 관련 HTTP 상태 코드를 함께 갖고 있다.GlobalRestControllerAdvice가 예외를 감지
@ExceptionHandler(UnauthorizedException.class)를 통해 UnauthorizedException 발생 시 이를 감지하고 처리@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<ApiResponse<ErrorResponse>> handleUnauthorizedException(UnauthorizedException e) {
ErrorResponse errorResponse = ErrorResponse.of(e.getErrorCode());
return ResponseEntity.status(e.getErrorCode().getHttpStatus())
.body(ApiResponse.error(errorResponse));
}
UnauthorizedException이 발생하면, handleUnauthorizedException() 메소드가 호출e.getErrorCode()를 통해 예외에 저장된 ErrorCode 객체를 가져오고, 이 정보를 바탕으로 ErrorResponse를 생성ErrorResponse 객체 생성
ErrorResponse는 ErrorCode를 바탕으로 생성public static ErrorResponse of(ErrorCode errorCode) {
return new ErrorResponse(
errorCode.getMessage(),
errorCode.getHttpStatus().value(),
LocalDateTime.now());
}
ErrorCode의 메시지(errorCode.getMessage()), HTTP 상태 코드(errorCode.getHttpStatus().value()), 발생 시간(LocalDateTime.now())이 ErrorResponse 객체에 저장ErrorResponse를 ApiResponse에 주입하고 응답 생성
ErrorResponse가 생성된 후, ApiResponse의 error 메소드를 사용해 ApiResponse 객체를 생성return ResponseEntity.status(e.getErrorCode().getHttpStatus())
.body(ApiResponse.error(errorResponse));
ApiResponse.error() 메소드는 success를 false로 설정하고, data는 null, error 필드에 ErrorResponse 객체를 포함시켜 ApiResponse 객체를 생성success는 false로 설정되고, ErrorResponse가 ApiResponse에 저장ErrorResponse가 있으면 실패 응답 반환, ErrorResponse가 없고 Data가 있으면 성공 응답 반환
ApiResponse는 error 메소드가 호출되었기 때문에 success는 false로 설정되고, error 필드에 ErrorResponse가 존재ApiResponse.success(data)가 호출되어 success는 true, data는 성공적으로 얻은 데이터 객체가 되고, error는 null이 된다.GlobalRestControllerAdvice가 적절한 응답을 클라이언트에게 전달
GlobalRestControllerAdvice는 ResponseEntity<ApiResponse<ErrorResponse>> 형태로 ApiResponse 객체를 클라이언트에게 반환ApiResponse를 통해 요청이 성공했는지(success), 에러가 발생했는지(error), 또는 성공적인 결과 데이터가 무엇인지(data)를 확인할 수 있다.Spring Boot 애플리케이션에서 일관된 예외 처리를 구현하면 예외 상황에 대한 관리를 효율적으로 할 수 있으며, 클라이언트와의 통신이 한결 수월해진다. 또한, 공통의 예외 처리 로직을 통해 중복된 코드를 줄이고, 가독성 높은 코드를 유지할 수 있다 !
이 포스팅에서는 GlobalExceptionHandler, ErrorCode, ErrorResponse, ApiResponse를 활용하여 Spring Boot 애플리케이션의 예외 처리 플로우를 어떻게 구성하고 관리할 수 있는지에 대해 설명했다 ! ! !