본문 바로가기
Study/spring

Spring Web MVC - Multipart 요청 다루기

by 유경호 2021. 2. 23.
반응형

지금 진행중인 토이 프로젝트에서 게시글 등록 시 게시글 내용 + 이미지 파일 업로드를 받는 로직을 구현하는데. 이에 대해서 간단히 정리하도록 하겠다.

 

일반적으로 Multipart 요청에 경우 Spring Web MVC에선 다음과 같은 방식을 제안한다.

@Controller
public class FileUploadController {

    @PostMapping("/form")
    public String handleFormUpload(@RequestParam("name") String name,
            @RequestParam("file") MultipartFile file) {

        if (!file.isEmpty()) {
            byte[] bytes = file.getBytes();
            // store the bytes somewhere
            return "redirect:uploadSuccess";
        }
        return "redirect:uploadFailure";
    }
}
  • 이미지 파일을 여러 개 받는 경우에 List<MultipartFile> 타입을 사용면 같은 파라미터 이름을 가진 이미지 파일들을 List<MultipartFile>에 바인딩해준다.
  • Map<String, MultipartFile> or MultiValueMap<String, MultipartFile>를 사용하면 파일 이름이 각각 다른 이미지 파일들을 바인딩할 수도 있다. 단, 이 경우에는 @RequestParam 의 name속성을 정의하지 않아야함.
  • Servlet 3.0 이상인 경우 MultipartFile 대신에 javax.servlet.http.Part를 사용할 수 있다.

 

form-data를 오브젝트에 바인딩하기

class MyForm {

    private String name;

    private MultipartFile file;

    // ...
}

@Controller
public class FileUploadController {

    @PostMapping("/form")
    public String handleFormUpload(MyForm form, BindingResult errors) {
        if (!form.getFile().isEmpty()) {
            byte[] bytes = form.getFile().getBytes();
            // store the bytes somewhere
            return "redirect:uploadSuccess";
        }
        return "redirect:uploadFailure";
    }
}
  • form데이터의 text 데이터들과 file 데이터를 한 번에 Object에 바인딩할 수있다.

 

아래와 같이 Json과 file을 같이 보내는 요청에 대해 각각 바인딩 받을 수 있다.

http 요청 예시

POST /someUrl
Content-Type: multipart/mixed

--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit

{
    "name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...

 

위의 요청을 핸들링하는 Controller 예시

@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata,
        @RequestPart("file-data") MultipartFile file) {
    // ...
}
  • @RequestPart를 사용하면 Json 파일로 넘어온 데이터를 바인딩할 수 있다.
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") MetaData metadata,
        BindingResult result) {
    // ...
}
  • javax.validation.Valid 나 스프링이 제공하는 @Validated 어노테이션을 붙여 사용할 수도 있음.
  • ErrorsBindingResult 로 에러를 핸들링할 수 있다.

검증

이제 위의 내용을 검증해보도록 하자. 검증할 내용은 두 가지이다.

  • form-data로 text 필드 데이터와 file 데이터를 한 번에 보내면 한 Object에 바인딩 하는 지
  • Json + file 형태로 보내는 요청의 데이터를 바인딩 하는 지

form-data의 field와 file을 한 번에 바인딩 받을 MyForm 클래스

public class MyForm {

    private String name;

    private MultipartFile file;

    ...

}

이미지 파일에 대한 MetaData를 Json을 통해 바인딩 받을 MetaData 클래스

public class MetaData {

    private String name;

    private String someString;

    private int someNumber;

    ...

}

검증을 위한 SampleController

@Controller
public class SampleController {

    @PostMapping("/form")
    @ResponseBody
    public String handleFormUpload(MyForm form, BindingResult errors) {
        System.out.println(form);
        return "Ok";
    }

    @PostMapping("/form/requestpart")
    @ResponseBody
    public String handleJsonAndFormUpload(@RequestPart("meta-data") MetaData metadata,
            @RequestPart("file-data") MultipartFile filedata) {
        System.out.println(metadata);
        System.out.println("[originalFileName=" + filedata.getOriginalFilename() + "]");
        return "Ok";
    }
}

테스트 코드 작성

@RunWith(SpringRunner.class)
@WebMvcTest(SampleController.class)
@AutoConfigureMockMvc
public class SampleControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    public void 파일업로드_테스트() throws Exception { // (1)
        MockMultipartFile image = new MockMultipartFile("file", "imagefile.jpeg", "image/jpeg", "<<jpeg data>>".getBytes());

        this.mockMvc
                .perform(multipart("/form")
                        .file(image)
                        .param("name", "value")
                        .contentType(MediaType.MULTIPART_FORM_DATA)
                        .accept(MediaType.APPLICATION_JSON)
                        .characterEncoding("UTF-8"))
                .andExpect(status().isOk());
    }

    @Test
    public void json_and_image_파일업로드_테스트() throws Exception { // (2)
        MockMultipartFile image = new MockMultipartFile("file-data", "filename-1.jpeg", "image/jpeg", "<<jpeg data>>".getBytes());

        String content = objectMapper.writeValueAsString(new MetaData("name", "stringSomething", 100));
        MockMultipartFile json = new MockMultipartFile("meta-data", "jsondata", "application/json", content.getBytes(StandardCharsets.UTF_8));

        this.mockMvc
                .perform(multipart("/form/requestpart")
                        .file(json) 
                        .file(image)
                        .contentType("multipart/mixed")
                        .accept(MediaType.APPLICATION_JSON)
                        .characterEncoding("UTF-8"))
                .andExpect(status().isOk());

    }
}
  1. form-data로 날아온 데이터들을 한 Object로 바인딩 받는 테스트
    • MyForm 클래스의 필드명과 form-data 요소들의 이름을 맞춰줌
  2. Json + 이미지 파일로 데이터를 구성한 요청을 바인딩하는 테스트
    • @RequestPart의 name 속성에 주입한대로 파일 이름을 맞춰줌

위의 테스트로 데이터들을 의도한대로 바인딩 되는 것을 볼 수 있었다. Controller에 작성한 System.out.println()에 아래와 같이 찍히는 것을 확인할 수 있었음.

MyForm [name=value, file=imagefile.jpeg]

MetaData [name=name, someString=stringSomething, someNumber=100]
[originalFileName=filename-1.jpeg]

참고

Spring Web MVC 공식문서 - https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-multipart

반응형