행운복권 프로젝트를 진행하면서 앱을 개발하신 분이 로그인 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을 잘못 입력했을 때 따로 예외를 잡을 수 없을까?
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