Post

[Spring] HttpMessageNotReadableException 해결

[1] 문제 상황

스프링에서 MockMvc를 사용해 컨트롤러 레이어를 테스트하던 중 문제가 발생했다. 내가 예상한 작동은 상태코드 200(Ok)를 리턴하는 것인데 400(Bad Request)과 함께HttpMessageNotReadableException 예외가 발생했다.

⬇️ 에러 로그

1
2
3
4
5
6
7
8
9
10
MockHttpServletRequest:
      HTTP Method = PUT
      Request URI = /api/v1/members/username
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", Accept:"application/json", Content-Length:"18"]
             Body = {"username":"New Username"}
    Session Attrs = {}
...
Resolved Exception:
             Type = org.springframework.http.converter.HttpMessageNotReadableException

⬇️ 테스트 코드: MemberControllerTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private MemberUsernameRequest memberUsernameRequest;

 @BeforeEach
public void setUp() {
    memberUsernameRequest = MemberUsernameRequest.builder()
                .username("New Username")
                .build();
}
...
@Test
void should_updateMemberUsername() throws Exception {
    Mockito.when(memberService.findMemberByOauthId(isA(String.class)))
            .thenReturn(member);
    Mockito.when(memberService.canUpdateUsername(isA(Member.class)))
            .thenReturn(true);
    Mockito.when(memberService.updateUsername(isA(Member.class), isA(MemberUsernameRequest.class)))
            .thenReturn(MemberUsernameResponse.of("New Username"));

    mockMvc.perform(put(MEMBER_BASEURL + "/username")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(memberUsernameRequest))
                    .with(user(OAuthUserPrincipal.of(member)))
                    )
                    .andExpect(status().isOk()); //에러 발생
}

테스트하고자 하는 컨트롤러는 username을 업데이트하는 기능을 한다. PUT 메서드를 사용하고, RequestBody에 username을 받는다.

⬇️ 메인 코드: MemberController

1
2
3
4
5
6
7
8
9
@Override
public ResponseEntity<MemberUsernameResponse> updateMemberUsername(OAuthUserPrincipal userPrincipal,
                                                                   MemberUsernameRequest memberUsernameRequest) {
    return new ResponseEntity<>(
                memberService.updateUsername(
                    memberService.findMemberByOauthId(userPrincipal.getOauthId(), memberUsernameRequest
                ),
                HttpStatus.OK);
}

인터페이스를 구현한 메서드 자체는 매우 간단하다. 서비스 레이어의 findMemberByOauthId 메서드를 호출하는 게 전부다.

⬇️ 메인 코드: MemberUsernameRequest

1
2
3
4
5
6
7
8
9
10
11
@Getter
public class MemberUsernameRequest {
    @JsonProperty("username")
    @NotNull
    private String username;

    @Builder
    public MemberUsernameRequest(String username) {
        this.username = username;
    }
}

[2] 발생 원인

스프링 공식 문서를 보면 HttpMessageNotReadableExceptionHttpMessageConverter.read(…)가 실패할 때 발생하는 에러라고 나와있다.

⬇️ HttpMessageConverter.read(…) 메서드에 대한 설명

1
2
3
4
5
6
7
8
9
10
11
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
        throws IOException, HttpMessageNotReadableException
Read an object of the given type from the given input message, and returns it.

Parameters:
clazz - the type of object to return. This type must have previously been passed to the canRead method of this interface, which must have returned true.
inputMessage - the HTTP input message to read from

Throws:
IOException - in case of I/O errors
HttpMessageNotReadableException - in case of conversion errors

⭐️ 정리하자면 HttpMessageConverter.read(…)는 입력 메시지(두번째 파라미터)를 읽어 주어진 타입(첫번째 파라미터)으로 반환하는 메서드이다. 두 가지 예외 중에 HttpMessageNotReadableException읽은 메시지를 주어진 타입으로 변환할 때 발생한다.

[3] 해결 방법

⬇️ 메인 코드: MemberUsernameRequest

1
2
3
4
5
6
7
8
9
10
11
12
@Getter
@NoArgsConstructor //추가
public class MemberUsernameRequest {
    @JsonProperty("username")
    @NotNull
    private String username;

    @Builder
    public MemberUsernameRequest(String username) {
        this.username = username;
    }
}

내가 작성한 코드에 대입해서 생각해보면, username을 변경할 때 호출하는 API에서 Request Body로 들어오는 값이 컨트롤러에 지정해준 타입 MemberUsernameRequest으로 변환되는 과정에 문제가 있었다. 기본 생성자가 없어서 객체 생성이 안되기 때문이었다. lombok을 사용하고 있기 때문에 @NoArgsContructor 어노테이션을 추가해서 해결하였다.

출처

This post is licensed under CC BY 4.0 by the author.