개요

스프링 컨트롤러에서 @RequestBody가 붙은 Dto 매핑 에러를 다룹니다. 스프링에서 ArgumentResolving 하는 로직을 확실하게 이해하고 있지 못했을 때라서 해당 에러를 만났을 때 긴 시간 삽질을 했습니다. 본 글에서는 제가 해당 에러를 다루면서 겪었던 과정과 그와 관련해서 스프링 컨텍스트 공부한 내용을 소개합니다.

에러 메시지와 코드 배경

09:37:13.054 [DEBUG] [XNIO-2 task-1] [.w.s.m.m.a.HttpEntityMethodProcessor] - Nothing to write: null body
09:37:13.055 [ WARN] [XNIO-2 task-1] [.a.ExceptionHandlerExceptionResolver] - Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public kr.co....CommonResponseEntity<java.lang.Void> kr.co....UserAuthController.test(kr.co...dto.TestDto)]

주요 메시지는 다음과 같습니다. HttpMessageNotReadableException은 @RequestBody 어노테이션이 붙은 매개변수에 대해 HTTP 요청의 본문이 없거나 잘못된 형식으로 인식되었을 때 나타나는 에러입니다. 스프링 MVC가 요청의 본문을 해당 Dto 객체로 변환하는 과정에서 예상치 못한 문제가 발생했음을 나타냅니다.

테스트를 위해 간략화된 별도의 엔드포인트와 dto와 요청을 다음과 같이 작성했습니다.

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1")
@Tag(name = "")
public class TestController {

    @PostMapping("/test")
    public CommonResponseEntity<Void> test(@RequestBody TestDto testDto) {
        return OK();
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TestDto {
    private String testId;
}

인텔리제이를 이용한 Http 요청

### test
POST <http://localhost:10002/ipms-user-managements/api/v1/test>
Content-Type: application/json

{
  "testId": "sampleId"
}

추적해 보니 예외가 발생한 클래스는 스프링 MVC 컨텍스트의 RequestResponseBodyMethodProcessor 였습니다.

RequestResponseBodyMethodProcessor 클래스는 스프링 MVC에서 @RequestBody 및 @ResponseBody 어노테이션을 처리하는 주요 역할을 하는데, HTTP 요청의 본문을 자바 객체로 변환하는(@RequestBody) 작업 중 에러가 발생한 것입니다.

문제 추적하기

문제가 된 메서드는 다음과 같았습니다.

@Override
    protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
            Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        Assert.state(servletRequest != null, "No HttpServletRequest");
        ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);

        Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
        if (arg == null && checkRequired(parameter)) {
            throw new HttpMessageNotReadableException("Required request body is missing: " +
                    parameter.getExecutable().toGenericString(), inputMessage);
        }
        return arg;
    }

해당 메서드에서

Object arg = readWithMessageConverters(inputMessage, parameter, paramType);

메시지 파싱시 arg가 null이 되어 예외가 발생했습니다.

readWithMessageConverters를 따라가 보니 AbstractMessageConverterMethodArgumentResolver로 연결되었는데 해당 클래스는 HandlerMethodArgumentResolver의 구현체중 하나였습니다.

해당 Resolver의 readWithMessageConverters메서드 중 문제가 된 부분은 다음과 같습니다.