글쓰기는 게시판의 형태를 가진 사이트의 기본적인 기능이라고 할 수 있다. Contents를 작성할 수 있는 form을 넣고, 그에 맞는 data를 server로 보내 DB에 저장하는 과정을 거치게 된다. 이 과정에서 필자의 실수로 그리 이상하지도 않은 어이없는 오류에 부딪혀서 꽤나 많은 시간을 소모했다. 하지만 반대로 새로 고민하게 된 내용도 존재하니, 그에 대한 이야기와 작업 내용을 기록하고자 한다.
우선 글을 쓰는 기능, 즉 contents를 작성하는 것은 기본적으로 form에 작성된 내용을 server로 보낸다는 것에서는 동일하다. 하지만 이전의 회원 가입 form에서 server로 보낼 때와의 차이점은 이번에는 file이 있다는 것이다.
private MultipartFile imageFile;
(Dto)
--------------------------------------------------------------------------------------
(Controller)
// (로고)사진 업로드 method
@RequestMapping(value = "/uploadImage.do", method = RequestMethod.POST)
@ResponseBody
public Map<String, Object> uploadImage(StoreDto dto, HttpServletRequest request){
// Tomcat 서버를 실행했을때 WebContent/upload 폴더의 실제 경로 얻어오기
String realPath=request.getServletContext().getRealPath("/upload");
String email = (String)request.getSession().getAttribute("email");
return service.uploadImage(realPath, dto, email);
}
위 코드는 국비 과정에서의 프로젝트에서의 controller method 중 일부를 가져왔다. 보이는 StoreDto에는 html의 input type file을 받을 수 있는 MultipartFile type field를 만들었다. 그리고 그 Dto 객체를 통해서 file data를 받아왔다. 하지만 이 게시글의 마지막에 작성해둘 모종의 문제로 인한 검색 중에 다른 사람들은 모두 Dto 객체에서 MultipartFile field를 분리해서 작성하고 있다는 것을 파악하였고, 해당 사항을 적용해보기로 했다.
var formData = new FormData(this);
var token = document.querySelector("meta[name=_csrf]").content;
var header = document.querySelector("meta[name=_csrf_header]").content;
var promise = fetch(url, {
method: "POST",
headers: {
"header": header,
"X-Requested-With": "XMLHttpRequest",
"X-CSRF-Token": token
},
body: formData //JSON.stringify(object)
});
(client form js)
---------------------------------------------------------------------------------------------------------
(server controller)
@PostMapping("/user/write")
public ResponseEntity<Menu> write(@ModelAttribute Menu menu, @RequestPart MultipartFile imageFile){
System.out.println(menu);
System.out.println(imageFile);
return ResponseEntity.ok().body(menu);
}
생각보다 단순하게 분리해주면 그만이었다. 이곳 을 함께 확인했다. 1
위 코드에서와 같이 client 쪽에서는 fetch로 body를 넘길 때 JSON으로 바꾸지 않고 form data를 그대로 넘기고, server 쪽에서는 controller에서 Dto object에 parsing 하고 싶다면 그 object와 함께 MultipartFile을 @RequestPart annotation을 붙여서 구분하여 data를 받는 방식이다. 이때 Dto object 앞의 @ModelAttribute annotation은 생략이 가능하다.
몇몇 다른 블로그에서의 경우 Data를 받을 Object와 MultipartFile 두 가지를 모두 @RequestPart annotation을 붙여 받는 경우도 볼 수 있었다. 2 하지만 굳이 @RequestPart annotation을 사용하지는 않아도 되며, 혹시라도 JSON 과 file을 동시에 넘길 상황을 생각하여 참고 블로그를 기록해둔다. 3
마주한 문제
이번에도 동일하게 다른 form element 들과 함께 한 번에 controller에 form data를 받아오고, 그것을 그대로 client로 return 하는 과정에서 다음과 같은 문제를 만났다.
Menu(contentId=null,
title=,
writer=,
content=,
imagePath=null,
category=null,
link=,
bePublic=false,
imageFile=org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@3d1cc7ae)
2022-11-24 02:21:24.365 ERROR 2352 --- [nio-8080-exec-4] 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.ByteArrayInputStream];
nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
No serializer found for class java.io.ByteArrayInputStream and no properties discovered to create BeanSerializer
(to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
(through reference chain: com.example.recipository.model.entity.Menu["imageFile"]
->org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile["inputStream"])
] with root cause
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
No serializer found for class java.io.ByteArrayInputStream and no properties discovered to create BeanSerializer
(to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
(through reference chain: com.example.recipository.model.entity.Menu["imageFile"]
->org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile["inputStream"])
Controller로 data는 잘 넘어오는데, 그것을 그대로 return 하는 과정에서 생긴 문제다. 사실 이 프로젝트에서 전혀 신경쓰지 않아도 되는 문제이고, 굳이 MultipartFile type을 field로 가지는 객체를 return 할 일도 없다. MultipartFile field를 제외한 객체를 return 할 때는 아무런 문제가 발생하지 않기도 했다.
해당 오류에 대해서 언급하고 있는 한 블로그를 발견했다. 블로그에서는 HttpMessageConversion을 하지 못하는, InputSream으로 MultipartFile 을 serialize 하지 못해서 발생하는 문제라고 언급하고 있다. 당장은 해당 문제를 해결할 단계가 아니라고 판단하고, 후에 참고할 수 있도록 footnote로 남겨둔다. 4
이 과정에서 관련 검색을 하다보면 다음과 같은 내용을 많이 볼 수 있었다.
- Object(객체)를 JSON data로 변환시키기 위해서는 Object에 getter method가 필요하다. 이때 사용되는 Object Mapper는 객체의 getter method를 사용하기 때문이다. 그리고 더하여 Constructor 또한 필요로 한다.
- Object에 @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) 를 작성해서 private field에도 접근할 수 있도록 한다.
두 번째 방법은 이미 getter method만 있으면 private field의 값도 얻을 수 있었고, 필자는 이미 lombok annotation을 통해서 constructor는 물론 getter method를 만든 셈이기때문에 해당 문제는 아니어서 다른 원인을 찾고 있었다. 결국 단순히 위에서 언급한 MultipartFile type이 seriaize 되지 못하다는 이유를 알게 된 것은 아쉽지만, 이 과정에서 client에서 file을 받아오는 MultipartFile field를 Entity 객체에서 분리해야겠다고 느낀 점은 얻어갈만한 것이라 생각한다.
Reference
- [Thymeleaf]HTML Form 태그로 서버에 Multipart 형식의 데이터 전송하기(+로그인 인증문제 해결) (tistory.com) [본문으로]
- Spring Boot | multipart/form-data 파일 업로드 ( + React , Axios, REST API, multiple files) (tistory.com) [본문으로]
- [SpringBoot] ajax를 통해 파일과 json 컨트롤러로 보내는 방법 (tistory.com) [본문으로]
- MultipartFile Restemplate 전송시 오류 - juhee's 개발과 일상 (lovia98.github.io) [본문으로]
'프로젝트 > Recipository' 카테고리의 다른 글
[Dev] 22.12.05. File Upload (임시, 재개발 예정) (0) | 2022.12.05 |
---|---|
[Dev] 22.12.04. 게시글 작성 항목 중 링크에 대하여(2) : 작업 (0) | 2022.12.04 |
[Dev] 22.12.02. 게시글 작성 항목 중 링크에 대하여(1) : 양방향 연관 관계 (0) | 2022.12.02 |
[Dev] 22.11.22. Logout (0) | 2022.11.22 |
[Dev] 22.11.22 로그인 과정에서 마주한 문제 (0) | 2022.11.22 |
댓글