본문 바로가기
프로젝트/Recipository

[Dev] 22.12.04. Content Write (게시글 작성) Form Data

by 규글 2022. 12. 4.

 글쓰기는 게시판의 형태를 가진 사이트의 기본적인 기능이라고 할 수 있다. 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] [각주:3] 하지만 굳이 @RequestPart annotation을 사용하지는 않아도 되며, 혹시라도 JSON 과 file을 동시에 넘길 상황을 생각하여 참고 블로그를 기록해둔다.

 

 

마주한 문제

 이번에도 동일하게 다른 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 할 때는 아무런 문제가 발생하지 않기도 했다.

 

 해당 오류에 대해서 언급하고 있는 한 블로그를 발견했다.[각주:4] 블로그에서는 HttpMessageConversion을 하지 못하는, InputSream으로 MultipartFile 을 serialize 하지 못해서 발생하는 문제라고 언급하고 있다. 당장은 해당 문제를 해결할 단계가 아니라고 판단하고, 후에 참고할 수 있도록 footnote로 남겨둔다.

 

 이 과정에서 관련 검색을 하다보면 다음과 같은 내용을 많이 볼 수 있었다.

  • 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

댓글