잘못된 Request body를 받았을 때 적절한 응답 내려주기

신연우·2023년 12월 9일
0

WIL

목록 보기
12/13

배경

이커머스 서비스를 운영하다보면 판매자가 반드시 우리가 개발한 제품만을 사용하지 않습니다. 서드 파티를 이용하거나 직접 API를 연동하는 판매자 유형도 존재합니다.

이 경우 저희 서버의 API를 연동하는 개발팀과의 협업이 필요한데요. 특히 API 통신 과정에서 많은 오류가 발생하고는 합니다.

기존에 API 요청이 들어올 때 request body에서 필수값이 누락되거나 자료형이 일치하지 않는 경우 Http message not readable.이라는 메시지를 응답으로 내려줬습니다.

그러다보니 무슨 문제가 있는 것인지 문의는 항상 들어왔고, 대응할 때도 어떤 필드가 문제였는지를 파악하기 위헤 데이터독 로그를 뒤져야 했습니다.

문득 이 응답 메시지에 어떤 필드가 문제였는지를 알려줄 수 있다면 해당 문의를 대응하기 위한 리소스를 줄일 수 있지 않을까란 생각이 들었습니다.

구현

HttpMessageNotReadableException

Request body의 데이터를 읽어오는 경우 HttpMessageConverter에 의해 데이터를 읽고 내부에 정의된 dto로 변환합니다.

Request body를 전달할 때 content: application/json을 사용하도록 설정하기 때문에 MappingJackson2HttpMessageConverter를 사용해 json -> dto로 변환해줍니다.

MappingJackson2CborHttpMessageConverter가 json -> dto를 호출할 때 사용하는 메서드를 타고 들어가보면 AbstractJackson2HttpMessageConverterreadJavaType 메서드에 도달하게 됩니다.

private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
		MediaType contentType = inputMessage.getHeaders().getContentType();
		Charset charset = getCharset(contentType);

		boolean isUnicode = ENCODINGS.containsKey(charset.name());
		try {
				// 생략
		}
		catch (InvalidDefinitionException ex) {
				throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
		}
		catch (JsonProcessingException ex) {
				throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage);
		}
}

JsonProcessingException이 발생하면 이를 내부적으로 HttpMessageNotReadableException으로 한 번 감싸서 throw 합니다.

따라서 필수값이 누락되거나 데이터 자료형이 잘못된 경우 HttpMessageNotReadableException이 발생하게 되고, 이를 핸들링하여 응답 메시지를 적절하게 수정해주면 됩니다.

예외 핸들링

fun handleHttpMessageNotReadableException(throwable: HttpMessageNotReadableException): ResponseEntity<ErrorResponse> {
    val cause = throwable.cause as? MismatchedInputException
            ?: return errorResponse(HttpStatus.BAD_REQUEST, "Http message not readable.")

    val mismatchedFieldNames = cause.path.mapNotNull { it.fieldName }
    if (mismatchedFieldNames.isEmpty()) {
        return errorResponse(HttpStatus.BAD_REQUEST, "Http message not readable.")
    }

    val responseMessage = "${mismatchedFieldNames.joinToString()} 필드가 누락되었거나 데이터 형식이 올바르지 않습니다. 확인 후 재요청해주세요."
    return errorResponse(HttpStatus.BAD_REQUEST, responseMessage)
}

private fun errorResponse(status: HttpStatus, message: String): ResponseEntity<ErrorResponse> {
    return ResponseEntity
        .status(status)
        .body(ErrorResponse(message = message))
}

가장 먼저 throwable.causeMismatchedInputException인지 확인해야 합니다. jackson-module-kotlin에서는 json 데이터를 파싱해 클래스의 프로퍼티에 매칭시키는 것에 실패한다면 MismatchedInputException을 던지기 때문입니다. 만약 모종의 다른 exception이 cause에 담긴다면 이번에 해결하려는 목적과는 다른 이유로 예외가 발생한 것이므로 기존과 동일하게 메시지를 내려줍니다.

그 다음 cause.path에서 fieldName을 통해 어떤 필드에 의해 JsonProcessingException이 발생했는지를 알아냅니다. 해당 필드명을 이용해 응답 메시지를 적절하게 바꿔서 내려주면 됩니다.

TMI

  • 원래는 MissingKotlinParameterException을 던졌으나 MismatchedInputException을 던지도록 변경되면서 MissingKotlinParameterException은 deprecated 되었습니다. (출처)
  • cause.path.mapNotNull { it.fieldName }를 하게 되면 필드명은 최대 한 개가 나오게 됩니다. 필수값을 두 개 누락했다고 하더라도 한 개의 필드명만 나오는 특징이 있습니다.
  • cause.path.from 에는 deserialization 하려고 한 class 정보가 들어있습니다. 이를 이용해 특정 dto에 한해서만 응답 메시지 개선 로직을 적용해보는 것도 가능합니다.

인사이트

  • SpringBoot의 기본 HttpMessageConverter 중 하나인 MappingJackson2HttpMessageConverter는 JsonParsingException이 발생하면 HttpMessageNotReadableException을 던진다.
  • HttpMessageNotReadableException.causeMismatchedInputException라면 특정 필드가 deserialization 될 때 에러가 발생했다는 것을 의미하고, 이 예외를 이용해 여러 가지 정보를 얻어 사용할 수 있다.
profile
남들과 함께하기 위해서는 혼자 나아갈 수 있는 힘이 있어야 한다.

0개의 댓글