Dev_Henry

[Spring] MultipartFile이 포함된 DTO requestBody로 요청받기. swagger 요청 본문

Web/Spring

[Spring] MultipartFile이 포함된 DTO requestBody로 요청받기. swagger 요청

데브헨리 2024. 2. 18. 19:50
728x90

현재 진행중인 프로젝트에서 하나의 이미지와 다른 값들을 받아서 방명록 객체를 만드는 api가 있다.

처음엔 다른 일반적인 요청을 받을때처럼 하나의 DTO를 정의해서 요청값으로 받았다.

 

다만 평소처럼 그냥 RequestBody를 적어주고 스웨거를 들어가면, 파일을 올릴수없고 다른 값들과 함께 Json 텍스트값을 입력받게 되어있는 것을 볼 수있다.  사진파일은 다른 값들처럼 JSON으로 보낼수 없다.

 

 

사진파일을 받기위해 MULTIPART_FORM_DATA  타입으로 요청을 받도록 했다.

@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class GuestBookRequest {
    String name;
    MultipartFile photo;
}

---

    @PostMapping(value = "",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<ResultResponse> createGuestbook(@RequestBody GuestBookRequest guestBookRequest) {
..
}

 

이렇게 만들고 요청을 보내보니 아래와 같은 에러가 발생한다.

해당 에러에 대해 찾아보니 @RequestBody 어노테이션의 문제였다.

해당 어노테이션은 HttpMessageConverter를 통해 요청데이터를 역직렬화하는데, 이것은 일반적으로 Json 또는 Xml데이터를 기대하고, multipart 는 처리하지 못한다.

 

rest방식의 개발만 하다보니, 폼데이터를 받을일이 잘 없었고, 자연스럽게 @ModelAttribute 는 요청 파라미터(쿼리스트링)를 객체로 바인딩하는 어노테이션으로만 생각했었다..쿼리스트링 방식의 파라미터 뿐 아니라 폼데이터를 받을수도 있으며, 이미지와 같은 파일을 받을때는 정석적인 방법이었다.

 

하지만 또 다른 문제가 생겼는데, 하나의 DTO안에 file도 같이 받을 경우, 이미지를 등록하지 않는 상황에서 문제가생겼다.

MultipartFile에 기본생성자가 없다고한다. 

이미지를 여러개받는 상황이라 List<>로 감싸져있다면 이미지를 등록하지 않아도 List가 null로 만들어지지만, MultipartFile에는 null이 들어갈수 없는듯하다. 

 

그래서 아래와같이 dto와 사진파일을 따로 분리하고, 사진은 required =false를 설정해주었다.

public ResponseEntity<ResultResponse> createGuestbook(
						@ModelAttribute GuestBookRequest guestBookRequest, 
                        @RequestParam(required = false) MultipartFile multipartFile) {
                        ..
                        ..
                       }

 

 

이렇게 하니 또 문제가 있었는데, (포스트맨이나 프론트에서 직접 설정을 해서 보내는 실험은 안해봤지만)

스웨거에서 데이터를 보내면 GuestBookRequest 내부의 필드들을 각각 폼데이터 필드로 보내는게 아니라, json모양의 하나의 덩어리로 보내 객체 바인딩에 실패했다.

 

DTO에 대해서는 @ModelAttribute를 사용하지 않고 일반적인 json을 받기위한 방법인, @RequestBody를 사용해도 안되는건 마찬가지였다. 사진을 보내기위해 'consumes = MediaType.MULTIPART_FORM_DATA_VALUE'를 붙여 폼 데이터로 보내기때문에 json으로 읽지 못한다..

consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}

이렇게 둘다 적어줘도 스웨거에서는 하나로만 선택이 가능했다..

 

 

이런 저런 방법을 찾아보다가 @RequestPart 에 대해 알게되었다.

@requestPart는 파일의 경우 MultiPartResolver를 사용해 가져오고, 다른 타입의 경우 요청의 Content-Type에 해당하는 HttpMessageConverter를 사용해 데이터를 가져온다. RequestBody(Json데이터) + ModeAttribute(Form데이터) 느낌으로 딱 지금 찾던 기능 같았다.

그래서 이번에는 아래와 같이 코드를 작성했다.

@PostMapping(value = "",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<ResultResponse> createGuestbook(@RequestPart @Valid GuestBookRequest guestBookRequest
        , @RequestPart(value = "photo", required = false) MultipartFile photo){
    guestbookService.createGuestbook(guestBookRequest,photo);
    return ResponseEntity.status(HttpStatus.CREATED).body(ResultResponse.of(ResultCode.CREATE_GUESTBOOK_SUCCESS));
}

 

근데 이번에는 또 다른 에러가 발생한다.ㅜ

 

원인은 스웨거에서 dto와 photo 두가지를 보낼때 각각의 Content-type을 따로 설정해주지 못하는 문제였다. 앞서 말했듯 @requestPart는 content-type 헤더의 타입을 보고 맞는 컨버터를 찾아 사용하는데, dto에 json이라는 타입이 따로 지정되지 않으니, multipart로 읽어오고, multipart/form-data요청에서는 각 파트의 기본값이 octet-stream이다.

 

그래서 postman을 사용해 아래와같이 dto에 타입을 명시해주면 잘 동작한다..

 

 

하지만... 우린 스웨거를 쓰고싶다!!!

 

말했듯이 dto에 대하여 type을 json으로 명시해주면 requestPart는 알아서 json컨버터를 호출해 역직렬화를 해준다.

하지만 스웨거에서는 이렇게 각각 타입을 지정해주는 기능을 지원해주지 않고, 그렇다면 type이 multipart인 요청에서 json형식의 문자열을 파싱할 수 있도록 도와줘야 한다. octet-stream타입을 받으면 objectMapper로 파싱해주는 컨버터를 등록해주자.

package com.meow.footprint.global;

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 {

    /**
     * Converter for support http request with header Content-Type: multipart/form-data
     */
    public 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;
    }
}

 

이렇게 컨버터를 등록하고나면 원하던데로 동작하고 스웨거를 사용할수도 있다.

 

참고 : https://middleearth.tistory.com/35

728x90
반응형