Multipart/form-data 데이터 받기
기본적인 게시물, 댓글 crud 기능을 제법 혼자 힘으로 구축할 수 있게 되었으니 다음으로는 파일 업로드 처리하는 것을 학습하고자 몇 개를 간단히 건들어도 참 어려운 에러나 이해하기 어려운 개념들이 넘쳐난다. 그래서 오늘 일단 진행한 과정을 복기하고자 한다.
multipart
Content-Type은 api 연동시에 보내는 자원을 명시하기 위해 한다.
요즘은 요청에 대해서 Content-Type은 application/json 타입인 것이 많다.
application/json은 RestFul API를 사용하게 되며 request를 날릴 때 대부분 json을 많이 사용하게 됨에 따라 자연스럽게 사용을 많이 하고 있다.
그리고 다른 방식인 application/x-www-form-urlencoded는 html의 form의 기본 Content-Type으로 요즘 자주 사용하지 않지만 여전히 사용하는 경우가 종종 존재한다.
이 두 방식의 차이점은 application/json은 {key: value}의 형태로 전송되지만 application/x-www-form-urlencoded는 key=value&key=value의 구조로전달된다.
즉 application/x-www-form-urlencoded는 보내는 데이터를 URL인코딩 이라고 부르는 방식으로 인코딩 후에 웹서버로 보내는 방식을 의미한다. 그리고 폼을 전송할 때 파일만 전송하지 않는다는 점을 해결해야 한다.
그렇기에 multipart/form-data 방식을 많이 쓰는데 이 방식은 다른 종류의 여러 파일과 폼 내용을 함께 전달할 수 있다.
여기서 신기한 점은 이미지 파일을 보낼 때 png, jpg 파일로 바로 보내는 것이 아니라, 이미지 파일도 문자로 구성되어 있기 때문에 이미지 파일을 문자로 생성하여 HTTP request body에 담아 서버로 전송하는 점이다.
그렇다면 multipart/form-data방식을 이용해서 이미지 파일을 전송을 시도하기 위해서 업로드 컨트롤러,dto를 생성해서 테스트를 해보자.
- uploadFileDTO
1
2
3
4
5
@Data
public class UploadFileDTO {
private List<MultipartFile> files;
}
- uploadController
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
@RestController
@Log4j2
public class UploadController {
@Value("${com.example.upload.path}")
private String uploadPath;
@Operation(summary = "UPLOAD POST", description = "POST 방식으로 파일 등록")
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public List<UploadResultDTO> upload(@ReuqestBody UploadFileDTO uploadFileDTO){
log.info(uploadFileDTO);
if(uploadFileDTO.getFiles() != null){
uploadFileDTO.getFiles().forEach( file ->{
String originalName = file.getOriginalFilename();
log.info(originalName);
});
}
return null;
}
}
이렇게 코드를 작성하고 서버를 실행해 Swagger UI로 파일을 업로드를 시도를 할려고 했으나 시도조차 못했다.
파라미터가 애초에 문자열을 받을 수 있게 설정이 되어 있어 파일 첨부를 할 수가 없었다.
그래서 데이터를 받아오는 방식을 잘못 설정했는지 다른 방법이 있나 검색을 해본 결과 총 4가지가 있었다.
@RequestBody
HTTP 요청으로 넘어오는 Body 의 내용을 HttpMessageConverter 를 통해 Java Object로 역직렬화한다.
바이너리 파일을 포함하고 있지 않은 데이터를 받는 역할을 한다.HttpMessageConverter : HTTP 요청과 응답에 대해서 “전략 패턴”을 사용해 Converting 해주는 역할
@RequestBody 어노테이션은 HTTP 요청으로 같이 넘어오는 Header 의 Content-Type을 보고 어떤 Converter 를 사용할지 정하므로 Content-Type 은 반드시 명시해야한다.
자주쓰는 Content-Type 종류
- application/json : { key : value } 형태인 json 형태로 전송
- application/x-www-form-urlencoded : name=obo&number=123456 형태인 쿼리 스트링 형태로 전송
- multipart/form-data : 파일 업로드시 사용되며, 파일을 포함한 여러 데이터가 쪼개서 형식으로 나눠서 전송
@RequestPart
Content-Type 이 multipart/form-data 에 특화된 어노테이션이며 MultipartFile 이 포함되는 경우에 MultipartResolver가 동작하여 역직렬화를 하게 된다.
- 역직렬화 : Byte로 되어있는 데이터를 객체 형태로 변환
MultipartFile 이 포함되지 않는 경우에 @RequestBody 와 같이 HttpMessageConverter가 동작된다.
multipart/form-data에 특화되어 여러 복잡한 값을 처리할 때 사용할 수 있는 어노테이션이다.
@RequestParam
HTTP 요청에서 하나의 파라미터를 받을 때 사용한다. 기본적으로 파라미터가 필수적으로 들어오게 설정되어 있기 때문에 파라미터가 들어오지 않을 경우 에러가 발생할 수 있다.
들어오지 않을수도 있다면 @RequestParam 의 required 설정을 false 로 설정한다.
@RequestParam 또한 @RequestPart 와 같이 MultipartFile 을 받을 때 사용할 수 있다.
@RequestPart 와 다른점은 @RequestParam 의 경우 파라미터가 String 이나 MultipartFile 이 아닌경우 Converter 나 PropertyEditor 에 의해 처리 된다.
하지만 @RequestPart 는 HttpMessageConverter 가 Content-Type 을 참고하여 알맞는 Converter 로 처리한다.
결국 쿼리 파라미터, 폼 데이터, Multipart 등 많은 요청 파라미터를 처리할 수 있는 어노테이션이다.
그래서 일단 @RequestPart,@RequestParam 사용해본 결과 모두 실패하였고, 실패 결과는 위의 @RequestBody사용했을 때 의 결과와 같았다.
@RequestParam 경우에는 하나의 파라미터가 들어가는 특성 때문에 이미지 파일 하나만 업로드 가능하며, 그 이상은 할 수 없게 되고 @Schma를 이용해서 타입이나 포맷을 바꿔서 그래도 파일 업로드는 할 수 있었으나 “Unsupported Media Type” 415에러가 나타나면서 Content-Type이 일치하지 않게 되었다.
- @RequestParam, @RequestParam를 사용했을 때
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
@Operation(summary = "UPLOAD POST", description = "POST 방식으로 파일 등록") @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public List<UploadResultDTO> upload(@RequestParam @Schema(type="file") UploadFileDTO uploadFileDTO){ log.info(uploadFileDTO); if(uploadFileDTO.getFiles() != null){ final List<UploadResultDTO> list = new ArrayList<>(); uploadFileDTO.getFiles().forEach( file -> { String originalName = file.getOriginalFilename(); log.info(originalName); }); } return null; }
물론 @RequestPart를 사용했을 때도 마찬가지로 파라미터가 하나의 파일 밖에 업로드되거나 Content-Type 일치하지 않는 에러가 동일하게 나타났다.
추측하기로는 문자열 리스트나 multipart 타입을 인자로 넣으면 인식이 되거니 싶어 바로 dto말고 앞의 두 개의 타입 변수를 넣어본 결과 첨부파일을 업로드할 수 있는 파라미터가 인식이 되었다.
즉 매개변수가 임의로 만든 dto 인자를 넣다 보니 Swagger UI에서는 인식을 하지 못하고 그냥 파라미터를 문자열로 받을 수 있게 처리 한 것 같았다.
@ModelAttribute
4가지 방식 중 마지막으로 남은 것은 @ModelAttribute 어노테이션이었는데, @ModelAttribute는 클라이언트로부터 일반 HTTP 요청 파라미터나 multipart/form-data 형태의 파라미터를 받아 객체로 사용하고 싶을 때 이용된다.
@ModelAttribute는 “가장 적절한” 생성자를 찾아 객체 생성 및 초기화 > 데이터 바인딩 > Validation 순서로 진행되며 데이터 바인딩은 getter/setter가 존재하는 변수에 한해서 이루어진다.
그래서 울며 겨자 먹기로 @ModelAttribute를 넣어서 Swagger UI를 실행한 결과 제대로 파일 첨부를 할 수 있는 업로드 기능이 제대로 인식되었고, 당연 여러 파일을 첨부할 수 있었고 업로드 또한 제대로 처리되었다.
뒷맛이 이상한 해결
그래서 이 @ModelAttribute 어노테이션을 사용해서 해결은 했지만, 대체 왜 나머지 @RequestPart,@RequestParam,@RequestBody이들과는 어떠한 차이 때문에 파라미터를 인식하는 경우를 잘 이해하지 못하였다.
그래서 해결한 뒤로도 다시 저 3개의 어노테이션들을 이용해서 요리저리 코드들을 추가하면서 찾아볼려고 하였으나 너무 이 문제에 함몰되는 것 같아 오늘은 제쳐 두고 진도를 나가기로 하였다.
그래서 일단은 이 요청 파라미터를 받는 방식들을 좀 더 어떠한 차이가 있는지 탐색을 해봐야 할 것 같다.