☀️ 이슈 설명
프로젝트 등록 기능에서 제목 , 내용 등과 같은 요청 데이터와 여러장의 이미지(MultipartFile)들을 같이 요청해서 등록 하는 API를
구현해야 했다. 추가로 image의 순서 값도 필요했기에 imageIndex라는 숫자 타입의 값도 받아야 한다. 처음에는 현재 프로젝트의 다른 요청 API 형태처럼(아래 코드 참고) dto를 만들어서 진행했는데 ExceptionHandlerExceptionResolver 형태의 에러가 발생했다.
@PostMapping(value ="/projects")
@Operation(description="프로젝트를 생성")
public ResponseEntity<Void> saveProject(@RequestBody ProjectRequestDto projectRequestDto)
{
//Controller 내용
}
해당 에러에 대해 찾아보니 MultipartFile 형태의 데이터를 받을 때는 @RequestBody 어노테이션 사용이 아니라는 걸 발견했습니다.
간단하게 Spring의 http요청 데이터 바인딩 어노테이션에 대해 설명 드리겠습니다.
- @RequestBody
- @RequestBody 어노테이션은 HTTP 요청의 본문(body)을 자바 객체(주로 DTO)로 변환합니다. 주로 JSON 또는 XML 형태로 전달된 데이터를 처리할 때 사용합니다. 단 클라이언트의 HTTP 요청 중 Body에서 바이너리 파일(Multipart)을 포함하고 있지 않은 데이터를 받기 때문에 , multipart/form-data가 포함되는 경우는 사용 할 수 없습니다. @RequetsBody는 HTTP 요청으로 같이 넘어오는 Header의 Content-type을 보고 어떤 Converter를 사용할지 정하기 때문에 클라이언트는 Content-type을 반드시 명시해야 합니다.
- @RequestBody 어노테이션은 HTTP 요청의 본문(body)을 자바 객체(주로 DTO)로 변환합니다. 주로 JSON 또는 XML 형태로 전달된 데이터를 처리할 때 사용합니다. 단 클라이언트의 HTTP 요청 중 Body에서 바이너리 파일(Multipart)을 포함하고 있지 않은 데이터를 받기 때문에 , multipart/form-data가 포함되는 경우는 사용 할 수 없습니다. @RequetsBody는 HTTP 요청으로 같이 넘어오는 Header의 Content-type을 보고 어떤 Converter를 사용할지 정하기 때문에 클라이언트는 Content-type을 반드시 명시해야 합니다.
- @ModelAttribute
- multipart//form-data 또는 formData(쿼리스트링)의 값을 받을 때 주로 사용되며 Spring Controller에서 값을 받을 때 사용되는 default 값입니다. 즉 , HTTP body로 오든 파라미터로 오든 다 받을 수 있고 body와 파라미터가 같이 오는 경우에도 데이터바인딩이 됩니다. 이런 형태가 가능한 이유는 @ModelAttribute가 필드 내부와 1:1로 값이 Setter나 Constructor를 통해 값이 매핑되기 때문입니다. 이 말은 바인딩하는 자바 객체에 Setter나 Constructor를 필수로 가지고 있어야 한다는 뜻입니다.
- multipart//form-data 또는 formData(쿼리스트링)의 값을 받을 때 주로 사용되며 Spring Controller에서 값을 받을 때 사용되는 default 값입니다. 즉 , HTTP body로 오든 파라미터로 오든 다 받을 수 있고 body와 파라미터가 같이 오는 경우에도 데이터바인딩이 됩니다. 이런 형태가 가능한 이유는 @ModelAttribute가 필드 내부와 1:1로 값이 Setter나 Constructor를 통해 값이 매핑되기 때문입니다. 이 말은 바인딩하는 자바 객체에 Setter나 Constructor를 필수로 가지고 있어야 한다는 뜻입니다.
- @RequestParam
- 한 가지 유형의 데이터를 받아올 수 있으며 파라미터가 Stirng이나 MultipartFile이 아닌 경우 Converter나 PropertyEditor 에 의해 처리 됩니다. 기본적으로 파라미터가 필수적으로 들어오게 설정되어 있기에 파라미터가 들어오지 않는 경우 BadRequest(400)이 발생하므로 파라미터가 들어올 수도 , 들어오지 않을 수도 있다면 required = false를 주어야 합니다.
요청 데이터의 개수가 많아지면 DTO를 생성해서 값을 매핑하는 것이 효율적입니다.
- 한 가지 유형의 데이터를 받아올 수 있으며 파라미터가 Stirng이나 MultipartFile이 아닌 경우 Converter나 PropertyEditor 에 의해 처리 됩니다. 기본적으로 파라미터가 필수적으로 들어오게 설정되어 있기에 파라미터가 들어오지 않는 경우 BadRequest(400)이 발생하므로 파라미터가 들어올 수도 , 들어오지 않을 수도 있다면 required = false를 주어야 합니다.
- @RequestPart
- application/json 또는 multipart/form-data 의 값을 받을 때 주로 사용되며 @RequestBody + multipart/form-data인 경우에 주로 사용됩니다. 클라이언트의 HTTP 요청 중 Body에서 MultipartFile(Binary Stream)이 포함되는경우에 multipart Resolver가 동작하여 역직렬화를 하게 됩니다. Content-type이 'multipart/form-data'와 관련된 경우에 사용하여 MultipartFile 이 포함되지 않는 경우에는 @RequestBody와 같이 HttpMessageConverter가 동작하게 됩니다. MultipartFile이 포함되지 않은 경우에는 @RequestBody와 같은 역할을 합니다.
이와 같이 @RequsetBody는 HttpMessageConverter를 통해 요청데이터를 역직렬화 하는데 , 이것은 일반적으로 Json 또는 Xml 데이터를 기대하고 , multipart는 처리하지 못합니다.
회사에서나 개인적으로나 rest방식의 개발만 주로 하다가 이미지 업로드 요청과 같은 폼데이터를 받았던 경험이 없어서 당연하게
@RequestBody로 진행해서 생긴 이슈입니다.
☀️ 해결 과정
@RequestBody의 문제라는 걸 확인하고 form-data를 받는 @ModelAttribute 로 변경을 했는데 스웨거에서 데이터를 보내면 ProjectRequestDto 내부의 필드들을 보내는 게 아니라 , json 모양의 하나의 덩어리로 보내 객체 바인딩을 실패하는 문제가 생겼다.
그래서 json형태의 데이터는 json 형태로 받고 multipart 파일은 form-data 형태로 받아서 구현을 해야 하는 상황이였다.
이러한 상황에서 @RequestPart는 파일의 경우 MultipartResolver를 사용해 가져오고 , 다른 타입의 경우 요청의 Content-Type에 해당하는 HttpMessageConverter를 사용해 데이터를 가져오기 때문에 현재 상황에서는 @RequestPart가 가장 적합하다 생각하여 이 방법을 선택했다.
변경된 코드는 아래와 같습니다!
@PostMapping(value ="/projects" , consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(description="프로젝트를 생성")
public ResponseEntity<Void> saveProject(@RequestPart ProjectRequestDto projectRequestDto,
@RequestPart List<MultipartFile> images,
@RequestPart List<Integer> imageIndexes,
@AuthenticationPrincipal CustomUserDetails customUserDetails)
위와 같이 구현 후 Swagger에서 API를 호출해보니
Content-Type 'application/octat-stream' is not supported
위의 에러가 하나 발생했다.
이 에러는 @RequestPart는 content-type 헤더의 타입을 보고 컨버터를 찾아 사용하는데 , ProjectRequestDto에 json이라는 타입이 스웨거에서 따로 지정되지 않아(PostMan에서는 자동으로 json 타입 지정이 있어서 문제가 생기지 않았다) , multipart로 읽어오고 , multipart/form-data요청에서는 각 파트의 기본값이 octet-stream이기 때문에 객체 형태로 바인딩을 하지 못하는 문제였다.
나는 프론트 팀원과 Swagger 위주로 협업하고 있어서 Swagger에서 문제가 없게 하기 위한 방안을 찾던 도중 Swagger를 특정 데이터에는 json 타입 특정 데이터에는 multipart 타입 이렇게 각각 타입을 지정해주는 기능을 지원해주지 않는다고 하여 , type이 multipart인 요청에서 json형식의 문자열을 파싱할 수 있도록 octet-stream타입을 받으면 objectMapper로 파싱해주는 컨버터를 등록해서 해결하였다.
package com.example.team_12_be.project.image;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.stereotype.Component;
import java.lang.reflect.Type;
@Component
public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
protected MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper,MediaType.APPLICATION_OCTET_STREAM);
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
protected boolean canWrite(MediaType mediaType) {
return false;
}
}
'TroubleShooting' 카테고리의 다른 글
| [FeedB 프로젝트] Java Collection 반복문에서 수정 또는 삭제 concurrentmodificationexception 에러 (0) | 2024.07.04 |
|---|---|
| [FeedB 프로젝트] SpringBoot 프로젝트에서 민감 정보 보안(.env 파일) (0) | 2024.07.04 |