RestTemplate으로 multipart file 전송 시 발생하는 HttpMessageConversionException

현재 진행 중인 프로젝트가 API 서버에 여러번 요청을 보내야 하나의 데이터가 생성되고 저장된다.
단순히 여러 요청을 보내면 수작업으로 일일이 요청을 보내는 것이 오히려 자동화를 하면서 드는 공수보다 적게 들 것이지만
이전 요청의 응답값이 다음 요청에 사용되기 때문에 그점이 번거로워 간단한 웹앱을 만들기로 했다.
(Postman을 통해 따로 만들고 말고 할 것도 없이 간단하게 자동화가 가능하지만 팀내에 Postman을 API request 기능 외에 다른 기능을 쓸 줄 아는 사람이 없어 모두 보고 이해할 수 있는 Spring Boot web app으로 결정했다.)

서문이 길었는데 지금 프로젝트의 API 요청을 할 때 multipart 파일을 전송해야 한다.
그렇기 때문에 웹앱에서 클라이언트(브라우저)로부터 파일을 받아서 이를 다시 API 서버로 전송해야한다.
이 포스팅은 이를 구현하는 도중에 발견한 Exception에 대한 것이다.

아래는 내용을 간소화시킨 코드이다.

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
26
27
28
29
30
31
32
33
34
35
36
@RestController
public class ApiRequestController {
private final ApiRequestService apiRequestService;

public ApiRequestController(final ApiRequestService apiRequestService) {
this.apiRequestService = apiRequestService;
}

@PostMapping("/request")
public String request(final String apiRequest, final MultipartFile file) {
return this.apiRequestService.request(apiRequest, file);
}

}

@Service
public class ApiRequestService {

public String request(final String apiRequest, final MultipartFile file) {
RestTemplate restTemplate = new RestTemplate();
return restTemplate.postForEntity("http://foo.bar/api", getRequest(apiRequest, file), String.class);
}

private HttpEntity<MultiValueMap<String, Object> getRequest(final String apiRequest,
final MultipartFile file) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);

MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("apiRequest", apiRequest);
body.add("file", file);

return new HttpEntity<>(body, headers);
}

}

이렇게 클라이언트로부터 받은 MultipartFile을 그대로 RestTemplate의 request로 사용할 경우 HttpMessageConversionException이 발생하면서 아래와 같은 Exception 로그가 나온다.
(너무 길어서 여러줄로 나눴다.)

1
2
3
4
5
6
7
8
9
10
11
12
2019-07-02 14:25:48.334 ERROR 29048 --- [nio-9090-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    :
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
[Request processing failed; nested exception is org.springframework.http.converter.HttpMessageConversionException:
Type definition error: [simple type, class java.io.FileDescriptor];
nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
No serializer found for class java.io.FileDescriptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain:
org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile["inputStream"]->java.io.FileInputStream["fd"])] with root cause

com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
No serializer found for class java.io.FileDescriptor and no properties discovered
to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
(through reference chain: org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile["inputStream"]->java.io.FileInputStream["fd"])

솔직히 원인은 아직 잘 모르겠다.
대신 구글링을 통해서 아래와 같이 ByteArrayResource 객체를 생성하여 이를 사용하는 방법을 찾았고
적용 시 위 Exception이 발생하지 않고 정상 작동하는 것을 확인할 수 있었다.
(급하게 만드느라 깊게 파보지 않았ㄷ…)

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
26
27
28
29
30
31
@Service
public class ApiRequestService {

public String request(final String apiRequest, final MultipartFile file) {
RestTemplate restTemplate = new RestTemplate();
return restTemplate.postForEntity("http://foo.bar/api", getRequest(apiRequest, file), String.class);
}

private HttpEntity<MultiValueMap<String, Object> getRequest(final String apiRequest,
final MultipartFile file) {

ByteArrayResource fileResource = new ByteArrayResource(file.getFile().getBytes()) {
// 기존 ByteArrayResource의 getFilename 메서드 override
@Override
public String getFilename() {
return "requestFile.wav";
}
};

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);

MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("apiRequest", apiRequest);
// body.add("file", file);
body.add("file", fileResource);

return new HttpEntity<>(body, headers);
}

}

출처: https://stackoverflow.com/questions/43596749/resttemplate-send-file-as-bytes-from-one-controller-to-another

Share