본문 바로가기

Back-End/Spring Boot

Spring Boot @RestControllerAdvice를 이용한 예외 처리 방법

Spring Boot에서 예외처리 할 때에는 단순하게 @ExceptionHandler에서 로직을 다 구현하여 처리해도 되지만 Front-End와 협업을 해야하는 입장에서 Error Message는 예쁘게, 담아야 하는 내용이 다 담겨있게 만들 수록 좋다.

 

나름 예쁘게 나올 수 있도록 코드를 순서대로 작성해 보았다.

1. Error 코드를 Enum으로 정의

전역적으로 사용되는 GlobalErrorCode와 특정 도메인에 대해 구체적으로 내려가는 UserErrorCode로 나누고, 인터페이스를 이용해 추상화한다.
public interface ErrorCode {
  HttpStatus getHttpStatus();
  String getMessage();
}
// Global Error Code Enum
@AllArgsConstructor
@Getter
public enum GlobalErrorCode implements ErrorCode{
  INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "내부 서버 오류가 발생하였습니다."),
  INVALID_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."),
  ;
  private final HttpStatus httpStatus;
  private final String message;
}
// User Error Code enum
@AllArgsConstructor
@Getter
public enum UserErrorCode implements ErrorCode{
  NOT_FOUND_USER(HttpStatus.NOT_FOUND, "유저를 찾을 수 없습니다."),
  ;
  private final HttpStatus httpStatus;
  private final String message;
}

2. Error Response 객체를 생성

팀에 맞는 Error메시지를 생성한다. 

record를 활용해서 작성해보았다.

// Error Response | record활용
@Builder
public record ErrorResponse(
// 에러 발생 시간
  LocalDateTime timeStamp,
// 상태 코드
  String statusCode,
// 상태 메시지
  String statusName,
// 요청 uri
  String requestUrl,
// 에러 메시지
  String message,
  // Field 에러 내용
  List<ValidationError> validationErrorList
) {

//  Field Valid Error
  @Builder
  public record ValidationError(String field, String message, String invalidValue) {
    public static ValidationError of(final FieldError fieldError) {
      return ValidationError.builder()
        .field(fieldError.getField())
        .message(fieldError.getDefaultMessage())
        .invalidValue(Optional.ofNullable(fieldError.getRejectedValue())
          .map(Object::toString)
          .orElse("null"))
        .build();
    }
  }
}

 

3. ErrorResponseService 인터페이스 생성

Error메시지를 내려주는 형태가 여러가지 있을 때, 알맞은 반환 방법을 정의한다.

// Error Service interface
public interface ErrorResponseService {
  ErrorResponse failure(ErrorCode errorCode, List<ErrorResponse.ValidationError> errorMessage, String uri);
  ErrorResponse failureWithNoContent(ErrorCode errorCode, String uri);
}

3-1. ErrorResponseServiceImpl 객체 생성

인터페이스에서 정의한 것을 토대로 구체화한다.

// Error Service impl
@Service
public class ErrorResponseServiceImpl implements ErrorResponseService {
  @Override
  public ErrorResponse failure(ErrorCode errorCode, List<ErrorResponse.ValidationError> errorMessage, String uri) {
    return ErrorResponse.builder()
      .timeStamp(LocalDateTime.now())
      .statusCode(errorCode.getHttpStatus().value() + "")
      .statusName(errorCode.name())
      .requestUrl(uri)
      .message(errorCode.getMessage())
      .validationErrorList(errorMessage)
      .build();
  }

  @Override
  public ErrorResponse failureWithNoContent(ErrorCode errorCode, String uri) {
    return failure(errorCode, new ArrayList<>(), uri);
  }
}

 

4. Custom Exception 생성(1)

이 클래스는 Custom Exception을 만들 때 ErrorCode의 get메서드를 강제해주기 위해 생성 하였다.
Custom Exception을 만들 때 GlobalException을 상속 받아서 사용하면 된다.
// GlobalException
@RequiredArgsConstructor
@Getter
public abstract class GlobalException extends RuntimeException {

    private final ErrorCode errorCode;

}

Custom Exception 생성(2)

위에서 만든 GlobalException을 상속받아서 실질적으로 사용한 CustomException을 생성하였다.

// UserException
@Getter
public class UserException extends GlobalException{
 
  private final ErrorCode errorCode;

  public UserException(ErrorCode errorCode) {
    super(errorCode);
    this.errorCode = errorCode;
  }

}

 

5. Exception 처리

@RestControllerAdvice를 사용하여 발생한 Exception들을 처리한다.

// RestControllerAdvice

@RequiredArgsConstructor
@RestControllerAdvice(basePackageClasses = RestApiController.class) // RestApiController에 대해서만
public class ApiControllerAdvice {

//  DI
  private final ErrorResponseService responseService;

//  Global Exception
  @ExceptionHandler(value = Exception.class)
  public ResponseEntity<?> exception(Exception e, HttpServletRequest request) {
    return returnDetails(INTERNAL_SERVER_ERROR, request.getRequestURI());
  }

//  UserException
  @ExceptionHandler(value = UserException.class)
  public ResponseEntity<?> userNotFoundException(UserException e, HttpServletRequest request) {
    return returnDetails(e.getErrorCode(), request.getRequestURI());
  }

//  Validation Exception
  @ExceptionHandler(value = MethodArgumentNotValidException.class)
  public ResponseEntity<?> methodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
    return returnDetails(INVALID_REQUEST, e, request.getRequestURI());
  }

//  content없는 Error ResponseEntity 생성
  private ResponseEntity<?> returnDetails(ErrorCode errorCode, String url) {
    ErrorResponse failure = responseService.failureWithNoContent(errorCode, url);
    return ResponseEntity.status(errorCode.getHttpStatus()).body(failure);
  }

//  content있는 Error ResponseEntity 생성
  private ResponseEntity<?> returnDetails(ErrorCode errorCode, BindingResult bindingResult, String url) {
    List<ErrorResponse.ValidationError> validationErrors = 
      bindingResult.getFieldErrors()
        .stream()
        .map(ErrorResponse.ValidationError::of)
        .toList();    
    ErrorResponse failure = responseService.failure(errorCode, validationErrors, url);
    return ResponseEntity.status(errorCode.getHttpStatus()).body(failure);
  }

}

 

6. Exception 결과

@RestController
@RequestMapping("/api")
@Validated
public class RestApiController {

  @GetMapping("/user/{id}")
  public String getUser(@PathVariable Long id) {
    if(id != 1){
      throw new UserException(NOT_FOUND_USER);
    }
    return id + "";
  }
  
  /*
{
    "timeStamp": "2024-06-10T03:28:41.3866596",
    "statusCode": "404",
    "statusName": "NOT_FOUND_USER",
    "requestUrl": "/api/user/2",
    "message": "유저를 찾을 수 없습니다.",
//    빈 배열 또한 중요한 정보가 될 수 있다고 생각해서 빈 배열로 리턴
    "validationErrorList": []
}
  */

  @PostMapping("/post")
  public User post(@RequestBody @Valid User user) {
    return user;
  }
  
  /*
{
    "timeStamp": "2024-06-10T03:27:53.5418522",
    "statusCode": "400",
    "statusName": "INVALID_REQUEST",
    "requestUrl": "/api/post",
    "message": "잘못된 요청입니다.",
    "validationErrorList": [
        {
            "field": "pw",
            "message": "\"^/d$\"와 일치해야 합니다",
            "invalidValue": "12d34"
        }
    ]
}
  
  */

}

 

'Back-End > Spring Boot' 카테고리의 다른 글

Spring Boot Interceptor란?  (0) 2024.06.17
Spring Boot Filter란  (2) 2024.06.16
AOP  (0) 2024.06.08
IoC와 DI  (0) 2024.06.08
Spring Boot Custom Validation만들기  (0) 2024.06.07