RestControllerAdvice를 이용하여 예외를 처리하는 이유는 스프링의 다양한 예외 처리 방법(ExceptionHandler, ControllerAdvice 등) 완벽하게 이해하기 - (1/2)이 블로그에서 너무나도 잘 설명해주었다. 따라서 나는 RestControllerAdvice를 이용해 예외를 처리하는 방법을 적용해보겠다.
@Getter
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
public class UserNotFoundException extends BusinessException {
public UserNotFoundException() {
super(ErrorCode.USER_NOT_FOUND_ERROR);
}
}
public class RoomNotFoundException extends BusinessException {
public RoomNotFoundException() {
super(ErrorCode.ROOM_NOT_FOUND_ERROR);
}
}
public User findUserById(Long userId){
return userRepository.findById(userId).orElseThrow(UserNotFoundException::new);
}
@Getter
@AllArgsConstructor
public enum ErrorCode {
// Global
INTERNAL_SERVER_ERROR(500, "G001", "서버 오류"),
INPUT_INVALID_ERROR(400, "G002", "잘못된 입력"),
// 채팅방
ROOM_NOT_FOUND_ERROR(404, "R001", "존재하지 않는 채팅방입니다."),
// 사용자
USER_NOT_FOUND_ERROR(404, "U001", "존재하지 않는 유저입니다.");
private final int status;
private final String code;
private final String message;
}
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ErrorResponse {
private int status;
private String code;
private String errorMessage;
private List<FieldException> fieldExceptions;
public static ErrorResponse of(ErrorCode errorCode){
return new ErrorResponse(errorCode, null);
}
public static ErrorResponse of(ErrorCode errorCode, BindingResult bindingResult){
return new ErrorResponse(errorCode, FieldException.of(bindingResult));
}
public ErrorResponse(ErrorCode errorCode, List<FieldException> fieldExceptions){
this.status = errorCode.getStatus();
this.code = errorCode.getCode();
this.errorMessage = errorCode.getMessage();
this.fieldExceptions = fieldExceptions;
}
}
List<FieldException>
은 아래에서 설명한다.이제 발생할 수 있는 Error를 @RestControllerAdvice
를 통해 처리를 하면 된다.
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// Business Error
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e){
log.warn(e.getMessage());
ErrorCode errorCode = e.getErrorCode();
ErrorResponse errorResponse = ErrorResponse.of(errorCode);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}
}
BusinessException
이 NotFoundException인 경우만 있어 일단은 HttpStatus를 NOT_FOUND로 했다.
- 다음과 같이 에러 처리가 되는 것을 확인할 수 있다.
// Default Error
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e){
log.error(e.getMessage(), e);
ErrorResponse errorResponse = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
INTERNAL_SERVER_ERROR(500, "G001", "서버 오류"),
같은 에러를 처리하기 위한 기본 에러 처리이다. Spring은 예외가 발생하면 가장 구체적인 예외 핸들러를 먼저 찾고, 없으면 부모 예외의 핸들러를 찾으므로 이 메서드에서 에러를 처리하게 된다.@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class UserLoginRequestDTO {
@NotBlank(message = "사용자 이메일(ID)은 필수 입니다.")
@Email
private String email;
@NotBlank(message = "사용자 비밀번호는 필수 입니다.")
private String password;
}
{ "email":"junsu123naver.com", "password": "123" }
// Valid Error
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e){
log.warn(e.getMessage());
ErrorResponse errorResponse = ErrorResponse.of(ErrorCode.INPUT_INVALID_ERROR, e.getBindingResult());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
public static ErrorResponse of(ErrorCode errorCode, BindingResult bindingResult){
return new ErrorResponse(errorCode, FieldException.of(bindingResult));
}
BindingResult.getFieldErrors()
를 통해 FieldError 로 이루어진 List 를 반환 받을 수 있다.private List<FieldException> fieldExceptions;
가 있었던 것이다.@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class FieldException {
private static final String NOT_INPUT_MESSAGE = "입력값이 없습니다.";
private String field;
private String value;
private String reason;
public static List<FieldException> of(BindingResult bindingResult){
List<FieldError> fieldExceptions = bindingResult.getFieldErrors();
return fieldExceptions.stream()
.map(error -> new FieldException(
error.getField(),
getValue(error),
error.getDefaultMessage()
))
.toList();
}
private static String getValue(FieldError error) {
if(error.getRejectedValue() == null){
return NOT_INPUT_MESSAGE;
}
return error.getRejectedValue().toString();
}
}
BindingResult.getFieldErrors()
를 통해 얻은 FieldError들을 처리할 객체가 필요하다.@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// Default Error
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e){
log.error(e.getMessage(), e);
ErrorResponse errorResponse = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
// Valid Error
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e){
log.warn(e.getMessage());
ErrorResponse errorResponse = ErrorResponse.of(ErrorCode.INPUT_INVALID_ERROR, e.getBindingResult());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
// Business Error
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e){
log.warn(e.getMessage());
ErrorCode errorCode = e.getErrorCode();
ErrorResponse errorResponse = ErrorResponse.of(errorCode);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}
}
- 다음과 같이 어디에서 어떤 값이 왜 잘못 되었는지 알 수 있다.
참고 :
[Spring] 스프링의 다양한 예외 처리 방법(ExceptionHandler, ControllerAdvice 등) 완벽하게 이해하기 - (1/2)
[Spring] @RestControllerAdvice를 이용한 Spring 예외 처리 방법 - (2/2)
[예외처리] @Valid 예외처리에 사용되는 BindingResult 객체는 무엇일까?
김기현 개발자 깃허브
유익한 자료 감사합니다.