본문 바로가기

Spring

HttpMessageNotReadableException, LocalDateTime

컨트롤러 단위 테스트를 완벽하게 짰다고 생각했는데 자꾸 HttpMessageNotReadableException가 발생했다.

분명 저번에 같은 문제를 해결했었는데.. 낚시로 물고기를 많이잡다보니 뇌가 물고기화 되가는 것 같다. 이번에 기록하고 기억해야겠다.

문제가 발생했던 상황을 최대한 간단하게 코드로 작성하면 아래와 같다.

@Test
void changeOrderStatus() throws Exception{
    Order order = createOrderWithOrderStatus(OrderStatus.MEAL);
    String content = new ObjectMapper().writeValueAsString(order);

    mockMvc.perform(put("/api/orders/{orderId}/order-status", "1")
            .content(content)
            .contentType(MediaType.APPLICATION_JSON))
            ...
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Enumerated(value = EnumType.STRING)
    private OrderStatus orderStatus;

    private LocalDateTime orderedTime;

    protected Order() {
    }
    ...

문제가 없어보인다. Order 객체를 생성한 후 Json형식으로 바꾸고 content 메서드에 담아서 보냈다. 그런데 계속 HttpMessageNotReadableException가 발생했다.

원인은 바로 LocalDateTime에 있었다.

ObjectMapper 객체로 LocalDateTime을 json으로 변환하면

orderedTime":{"month":"NOVEMBER","year":2020,"dayOfMonth":18,"hour":23,"minute":16,"monthValue":11,"nano":991000000,"second":56,"dayOfWeek":"WEDNESDAY","dayOfYear":323,"chronology":{"id":"ISO","calendarType":"iso8601"}

이렇게 변환된다. 이 값을 @RequestBody Order order로 받으려하니깐 안받아지고 HttpMessageNotReadableException가 터진 것 같다.

정리해보면 LocalDateTime이 직렬화/역직렬화가 안되는 것이 아니라 @RequestBody로 받아오는 객체에서 원하는 형태로 변환되지 않기 때문에 HttpMessageNotReadableException가 발생하는 것이다.

해결 방법은 간단하다. 나의 경우엔 테스트에서만 ObjectMapper를 사용해서 간단하게 LocalDateTime을 json으로 변환하는 것을 원했기때문에 아래와 같은 방법을 사용했다.

@Test
void changeOrderStatus() throws Exception{
    Order order = createOrderWithOrderStatus(OrderStatus.MEAL);
    String content = new ObjectMapper()
                .registerModule(new JavaTimeModule())
                .writeValueAsString(order);

    mockMvc.perform(put("/api/orders/{orderId}/order-status", "1")
            .content(content)
            .contentType(MediaType.APPLICATION_JSON))
            ...

핵심은 @RequestBody로 받아오는 객체에서도 읽을 수 있게 형식을 지정해주는 것이다. 위의 경우엔 .registerModule(new JavaTimeModule())로 형식을 지정해줬기에 아까와는 다르게 아래와 같은 형태로 json이 변환된다.

orderedTime":[2020,11,18,23,37,6,99000000]

이렇게 변환된 json은 @RequestBody로 충분히 받아올 수 있다.

ObjectMapper를 사용하지않고 아래와 같은 방식들로 json 데이터를 만들어서 요청을 보내는 경우엔

// 이렇게 json형식을 만들어 get 요청
String url = "/params?orderedTime=2020-11-18T11:00:00";

// 이렇게 json형식을 만들어 content에 담아 요청
 .content("{\"orderedTime\":\"2020-11-18T11:00:00\"}"));

Entity의 LocalDateTime변수에 직접 @DataTimeFormat 이나 @JsonFormat 어노테이션을 사용해서 쉽게 해결할 수 있다.

관련해서는 킹졸두님의 글에 설명이 너무 잘되어있다. 필요한 상황에 보고 찾아서 적용하면 좋겠다.