내가 원하는 예외가 아닌 HttpMessageNotReadableException만잡는다구요?

2024-02-10
  • Tech


레코드를 DTO로 사용하며 컴팩트 생성자를 이용해 요청 값에 대한 검증을 하려고 하였습니다.

public record LoginRequest(  
		String email,  
		String password  
) {  
  
	public LoginRequest {  
		required("email", email);  
		required("password", password);  
	}  
}
public static void required(String fieldName, String value) {  
	if (!StringUtils.hasText(value)) {  
		throw new RequestFieldException(MUST_BE_REQUIRED, fieldName);  
	}  
}

레코드의 컴팩트 생성자는 생성자 안에 있는 코드를 초기화 전에 호출해 줍니다. 그렇기 때문에 당연히 초기화 전에 예외가 발생하면 @RestControllerAdvice@ExceptionHandler에서 예외가 핸들링 되어 다음과 같은 응답이 오는 것을 예상하였습니다.

{
	"timeStamp": "2024-02-23 23:02:00",
	"status": "ERROR",
	"message": "email 필드는 값이 없을 수 없습니다.",
	"cause": "RequestFieldException",
	"errorCode": "MUST_BE_REQUIRED"
}

하지만 정작 제가 받은 응답은 아래의 응답 입니다.

wrong response

required()에서 던진 RequestFieldException이 아닌 DispatcherServlet이 던지는 HttpMessageNotReadableException 이 응답으로 오는 것입니다.


뭐가 문제일까?

원인은 JacksonJsonJava 문법으로 역직렬화 하는 과정에 있었습니다.

jackson StdValueInstantiator

위 코드는 DispatcherServletJson을 역직렬화 할 때 사용하는 Jackson라이브러리의 StdValueInstantiator입니다.

코드에서 볼 수 있듯 Object를 생성하는 _withArgsCreator.call(args)에서 발생하는 모든 예외를 잡아 예외를 던지고 있습니다. 이때 DispatcherServlet은 이 예외를 HttpMessageNotReadableException으로 던지고 있었던 것입니다. 그래서 @RestControllerAdvice에서도 제가 의도한RequestFieldException가 아닌 HttpMessageNotReadableException를 잡아 클라이언트로 응답을 해 줬던 것이죠.


더 정확한 확인을 위해 생성자에 브레이크 포인트를 걸어 디버깅을 해보면

debug break point

debugging result

_withArgsCreator.call(args) 에서 예외가 발생해 catch 문으로 넘어가는 것을 볼 수 있었습니다.


어떻게 해결하지?

원래 HttpMessageNotReadableException를 처리하던 exceptionHandler입니다.

@ExceptionHandler(HttpMessageNotReadableException.class)  
private ResponseEntity<ApiResponse> handleHttpMessageNotReadableException(  
HttpMessageNotReadableException e) {  
	ErrorCode errorCode = INVALID_HTTP_REQUEST;  
  
	return ResponseEntity.status(errorCode.getHttpStatus())  
		.body(ApiResponse.error(getCause(e), errorCode.name(), errorCode.getMessage()));  
}

예외를 감지하면 그에 대응하는 적절한 응답을 주는 단순한 방법 이였습니다.


그리고 제가 이 문제를 해결한 코드입니다.

@ExceptionHandler(HttpMessageNotReadableException.class)  
private ResponseEntity<ApiResponse> handleHttpMessageNotReadableException(  
HttpMessageNotReadableException e) {  
  
	if (e.getRootCause() instanceof RequestFieldException requestFieldException) {  
		ErrorCode requestFieltErrorCode = requestFieldException.getErrorCode();  
  
		return ResponseEntity.status(requestFieltErrorCode.getHttpStatus())  
			.body(ApiResponse.error(  
				getCause(requestFieldException),  
				requestFieltErrorCode.name(),  
				requestFieldException.getErrorMessage()  
			));  
	}  
  
	ErrorCode errorCode = INVALID_HTTP_REQUEST;  
  
	return ResponseEntity.status(errorCode.getHttpStatus())  
		.body(ApiResponse.error(getCause(e), errorCode.name(), errorCode.getMessage()));  
}

e.getRootCause()를 이용해 예외가 발생한 원인을 불러왔습니다.
HttpMessageNotReadableException은 이 경우를 제외하고도 자주 발생할 수 있는 예외이기 때문에 rootCauseRequestFieldException인 경우에 한해서만 별도의 응답을 제공하도록 하였습니다.

그 결과 이제 제가 예상하던 응답을 확인할 수 있게 되었습니다!!

final response


한 가지 아쉬운 점은

public LoginRequest {  
	required("email", email);  
	required("password", password);  
}  

{필드명} 필드는 값이 없을 수 없습니다 와 같은 응답을 전달하기 위해 위 코드 처럼 필드 명을 함께 전달해야 해서 약간은 불편한 느낌이 없지 않아 있어 이 부분을 더 ‘깔꼼!’하게 해결할 수 있도록 해보겠습니다.

Profile picture

wwan13

주니어 백앤드 개발자 김태완 입니다.