개발을 진행하다보면, 코드레벨에서 발생하는 Exception을 처리하게 됩니다. 하지만, 이러한 Exception은 일일히 try-catch와 @ExceptionHandler로 처리해야 할까요?
이를 해결하고자 반복적인 작업을 줄이고 전역에서 공통으로 Exception을 효율적으로 처리할 수 있는 Global Exception Handler
에 대해 알아보겠습니다.
Global Exception Handler
은 뜻 그래로 애플리케이션 전역에서 예외를 처리하고 사용자에게 응답을 제공하는 메커니즘입니다. 이를 통해 예외 처리 코드를 중앙에서 관리하고 예외에 대한 일관된 응답을 생성할 수 있습니다.
@ControllerAdvice
/ @RestControllerAdvice
과 @ExceptionHandler 어노테이션을 기반으로 Controller 내에서 발생하는 에러에 대해서 해당 핸들러에서 캐치하여 오류를 발생시키지 않고 응답 메시지로 클라이언트에게 전달해 주는 기능을 의미합니다.
@ExceptionHandler, @ModelAttribute, @InitBinder 가 적용된 메서드들에 AOP를 적용해 Controller 단에 적용하기 위해 고안된 어노테이션이라고 합니다.
클래스에 선언하면 되며, 모든 @RestController
에 대한, 전역적으로 발생할 수 있는 예외를 잡아서 처리할 수 있습니다.
@RestControllerAdvice
public class GlobalExceptionHandler {
// ErrorCode내의 에러
@ExceptionHandler(BusinessException.class)
protected ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
log.error("BusinessException", e);
ErrorResponse errorResponse = ErrorResponse.of(e.getErrorCode().getHttpStatus(),
e.getErrorCode().getErrorCode(), e.getErrorCode().getMessage());
return ResponseEntity.status(e.getErrorCode().getHttpStatus()).body(errorResponse);
}
// 나머지 에러
@ExceptionHandler(Exception.class)
protected ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("Exception", e);
ErrorResponse errorResponse = ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.toString(), e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
}
위와 같이 @RestConrollerAdvice와 @ExceptionHandler를 활용하여 전역에서 Exception을 처리했습니다.
@ExceptionHandler는 메서드에 선언하거나 특정 예외 클래스를 지정해주면 해당 예외가 발생했을 때 메서드에 정의한 로직으로 처리할 수 있습니다. 또한, @RestControllerAdvice에 정의된 메서드가 아닌 일반 컨트롤러 단에 존재하는 메서드에 선언할 경우, 해당 Controller에만 적용됩니다.
위에서 작성한 BusinessException은 아래와 같은 클래스로 작성되어 있습니다.
@Getter
public class BusinessException extends RuntimeException {
private ErrorCode errorCode;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
@Getter
public enum ErrorCode {
// 인증 && 인가
TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "A-001", "토큰이 만료되었습니다."),
NOT_VALID_TOKEN(HttpStatus.UNAUTHORIZED, "A-002", "해당 토큰은 유효한 토큰이 아닙니다."),
NOT_EXISTS_AUTHORIZATION(HttpStatus.UNAUTHORIZED, "A-003", "Authorization Header가 빈 값입니다."),
NOT_VALID_BEARER_GRANT_TYPE(HttpStatus.UNAUTHORIZED, "A-004", "인증 타입이 Bearer 타입이 아닙니다."),
REFRESH_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "A-005", "해당 refresh token은 존재하지 않습니다."),
REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "A-006", "해당 refresh token은 만료됐습니다."),
NOT_ACCESS_TOKEN_TYPE(HttpStatus.UNAUTHORIZED, "A-007", "해당 토큰은 ACCESS TOKEN이 아닙니다."),
NO_PERMISSION(HttpStatus.UNAUTHORIZED, "A-008", "권한 없음"),
FORBIDDEN_ROLE(HttpStatus.FORBIDDEN, "A-009", "해당 Role이 아닙니다."),
UNAUTHORIZED_MEMBER(HttpStatus.UNAUTHORIZED, "A-001", "회원 정보가 일치하지 않습니다."),
// 유저
NOT_EXISTS_USER_ID(HttpStatus.NOT_FOUND, "U-001", "존재하지 않는 유저 아이디입니다."),
NOT_EXISTS_USER_NICKNAME(HttpStatus.NOT_FOUND, "U-002", "존재하지 않는 유저 닉네임입니다."),
NOT_EXISTS_USER_EMAIL(HttpStatus.NOT_FOUND, "U-003", "존재하지 않는 유저 이메일입니다."),
ALREADY_REGISTERED_USER_ID(HttpStatus.BAD_REQUEST, "U-004", "이미 존재하는 유저 아이디입니다."),
NOT_EXISTS_USER_PASSWORD(HttpStatus.NOT_FOUND, "U-005", "존재하지 않는 유저 비밀번호입니다."),
INVALID_USER_DATA(HttpStatus.BAD_REQUEST, "U-006", "잘못된 유저 정보입니다."),
INVALID_ADMIN(HttpStatus.BAD_REQUEST, "U-007", "Admin은 제외 시켜주세요."),
private final HttpStatus httpStatus;
private final String errorCode;
private final String message;
ErrorCode(HttpStatus httpStatus, String errorCode, String message) {
this.httpStatus = httpStatus;
this.errorCode = errorCode;
this.message = message;
}
}
Error Code
는 공통적으로 사용될 수 있는 것들을 모아 enum 형태로 작성하였습니다. 또한, 동일한 Http Status Code를 가지고 있다면 정확하게 어떤 원인인지 알지 못하기 때문에 추가적으로 Code
와 Message
를 작성하여 효율적으로 Exception 처리를 진행했습니다.
피드백 및 개선점은 댓글을 통해 알려주세요😊