스프링부트 ResponseEntityExceptionHandler 적용

췌누의 개발·2024년 5월 27일
post-thumbnail

행운복권 프로젝트를 진행하면서 앱을 개발하신 분이 로그인 api를 사용할 때 지속적으로 알 수 없는 예외가 발생한다고 전달받았다.

서버에서 로그를 확인해 봤을 때 NoResourceFoundException이 계속해서 발생했다. NoResourceFoundException에 대해서 찾아봤지만 자료가 매우 부족했다. 서버에서 테스트했을 때는 잘 작동했기 때문에 아마도 프론트 쪽에서 문제가 발생했을 수도 있다고 생각했다.

그렇지만 프론트 코드에는 문제가 없는 것 같다고 하셨고 문제는 점점 미궁에 빠졌다. 다음날
로그인 api를 사용할 때 url에 알파벳 하나가 누락돼서 예외가 발생한 것으로 결론이 났다. 서버에서 이러한 예외를 잡아서 명확하게 전달했다면 빠르게 문제를 해결할 수 있었겠다고 생각했다.


현재 서버 코드를 확인해 보면,,,

GlobalExceptionHandler

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    @ExceptionHandler(LuckLotteryException.class)
    public ResponseEntity<ErrorResponse> luckLotteryExceptionHandler(
            LuckLotteryException e, HttpServletRequest request) {

        ErrorCode code = e.getErrorCode();
        ErrorResponse errorResponse =
                new ErrorResponse(
                        code.getStatus(),
                        code.getReason(),
                        request.getRequestURL().toString());
        return ResponseEntity.status(HttpStatus.valueOf(code.getStatus())).body(errorResponse);
    }

    @ExceptionHandler(Exception.class)
    protected ResponseEntity<ErrorResponse> handleException(Exception e, HttpServletRequest request)
            throws IOException {

        StringBuffer requestURL = request.getRequestURL();
        String queryString = request.getQueryString();

        if (queryString != null) {
            requestURL.append('?').append(queryString);
        }

        String url = requestURL.toString();

        log.error("INTERNAL_SERVER_ERROR", e);
        ErrorCode internalServerError = ErrorCode.INTERNAL_SERVER_ERROR;
        ErrorResponse errorResponse =
                new ErrorResponse(
                        internalServerError.getStatus(),
                        internalServerError.getReason(),
                        url);

        return ResponseEntity.status(HttpStatus.valueOf(internalServerError.getStatus()))
                .body(errorResponse);
    }

}

LuckLotteryException이 아닌 예외가 발생하면 INTERNAL_SERVER_ERROR로 처리를 해놨다.


그렇기 때문에 URL을 잘못 입력했을 때 INTERNAL_SERVER_ERROR와 NoResourceFoundException이 로그에 찍히는 것으로 알 수 있었다. url을 잘못 입력했을 때 따로 예외를 잡을 수 없을까?



HttpRequestMethodNotSupportedException

Spring에서 에러 핸들링에 대해서 ResponseEntityExceptionHandler 추상 클래스를 제공한다.


ResponseEntityExceptionHandler

public abstract class ResponseEntityExceptionHandler implements MessageSourceAware {

//.......
   
    @ExceptionHandler({HttpRequestMethodNotSupportedException.class, 
    HttpMediaTypeNotSupportedException.class, 
    HttpMediaTypeNotAcceptableException.class, 
    MissingPathVariableException.class, 
    MissingServletRequestParameterException.class, 
    MissingServletRequestPartException.class, 
    ServletRequestBindingException.class, 
    MethodArgumentNotValidException.class, 
    HandlerMethodValidationException.class, 
    NoHandlerFoundException.class, 
    NoResourceFoundException.class, 
    AsyncRequestTimeoutException.class, 
    ErrorResponseException.class, 
    MaxUploadSizeExceededException.class, 
    ConversionNotSupportedException.class, 
    TypeMismatchException.class, 
    HttpMessageNotReadableException.class, 
    HttpMessageNotWritableException.class, 
    MethodValidationException.class,
    BindException.class})
    @Nullable
    public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {.......}
    
  
// ........
              
}

코드 내부를 확인해 보면 여러 에러에 대한 핸들링을 제공한다.


GlobalExceptionHandler

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler{

//......
   
    @Override
    protected ResponseEntity<Object> handleNoResourceFoundException(NoResourceFoundException ex,
    																		HttpHeaders headers, 
    																		HttpStatusCode status, 
    																		WebRequest request) {
        return super.handleNoResourceFoundException(ex, headers, status, request);
    }
}

우리는 url이 잘못됐을 때 에러를 핸들링 하고자 하기 때문에 handleNoResourceFoundException 메서드를 오버라이딩 하여 구현했다.

테스트를 진행해 봤다. 그렇지만 오버라이딩만 했을 때는 우리 행운복권의 예외 응답 스펙과 다르고 직관적으로 봤을 때 어떤 예외가 발생했는지 알 수 없었다. 예외 응답 스펙에 맞춰 일괄적으로 예외를 처리하고자 한다.


ErrorResponse

@Getter
public class ErrorResponse {

  private final boolean success = false;
  private final int status;
  private final String reason;
  private final LocalDateTime timeStamp;
  private final String path;

  public ErrorResponse(int status, String reason, String path) {
      this.status = status;
      this.reason = reason;
      this.timeStamp = LocalDateTime.now();
      this.path = path;
  }
}

행운복권의 예외 응답 스펙이다. 예외 상태 코드, 예외 메시지, 예외 발생 시간, 예외 발생한 경로를 클라이언트로 제공한다.


ErrorCode

@Getter
@AllArgsConstructor
public enum ErrorCode {

    //......
    
    /* 404 NOT_FOUND : Resource를 찾을 수 없음 */
    URL_INPUT_ERROR(404,"url 입력 오류입니다"),
    ;

    private int status;
    private String reason;
}

에러코드와 메시지를 enum 통해서 따로 관리할 수 있도록 했다.



GlobalExceptionHandler

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler{

    
  //......
  
   @Override
   protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex,
                                                                         HttpHeaders headers,
                                                                         HttpStatusCode status,
                                                                         WebRequest request) {

        ServletWebRequest servletWebRequest = (ServletWebRequest) request;
        String url = servletWebRequest.getRequest().getRequestURI();
        ErrorResponse errorResponse = new ErrorResponse(status.value(), METHOD_NOT_ALLOWED.getReason(), url);
        return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(errorResponse);
    }


    @Override
    protected ResponseEntity<Object> handleNoResourceFoundException(NoResourceFoundException ex,
                                                                    HttpHeaders headers,
                                                                    HttpStatusCode status,
                                                                    WebRequest request) {
        ServletWebRequest servletWebRequest = (ServletWebRequest) request;
        String url = servletWebRequest.getRequest().getRequestURI();
        ErrorResponse errorResponse = new ErrorResponse(status.value(), URL_INPUT_ERROR.getReason(), url);
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
    }

}

API 요청 시 url이 잘못됐을 때, HTTP 메서드 오류 예외가 발생했을 때 핸들링 하여 예외 응답 스펙으로 예외를 제공하도록 구현했다.



발생한 예외를 행운복권 예외 응답 스펙으로 잘 전달한 것을 확인할 수 있었다. 사소한 예외라도 서버에서 프론트에 명확하게 전달하는 것이 중요하다는 것을 느꼈다.


프로젝트 링크를 통해서 참고하시면 좋을 것 같습니다! 감사합니다. 도움이 되셨으면 좋겠습니다.👋🏼

행운 복권 깃허브 링크
https://github.com/Uttug-Seuja/luck-lottery-server

profile
아샷추를 좋아합니다

0개의 댓글