본문 바로가기

Backend Development/Spring boot

[Spring boot] MultipartFile 파일 업로드 구현

Multipart  전송이란?

 

파일 업로드를 구현할 때, 클라이언트가 웹브라우저라면 폼을 통해서 파일을 등록해서 전송하게 된다. 이때 웹 브라우저가 보내는 HTTP 메시지는 Content-Type 속성이 multipart/form-data 로 지정되며, 정해진 형식에 따라 메시지를 인코딩하여 전송한다. 이를 처리하기 위한 서버는 멀티파트 메시지에 대해서 각 파트별로 분리하여 개별 파일의 정보를 얻게 된다. (From Wireframe)

 

이미지 파일을 전송한다고 해서 이메일에 첨부파일을 붙여 메일을 보내는 것처럼 png나 jpg 파일 자체가 전송되는 것이 아니다. 이미지 파일도 문자로 이뤄져 있기 때문에 이미지 파일을 스펙에 맞게 문자로 생성하여 HTTP request body에 담아 서버로 전송하는 것임.

HTTP(request와 response)는 간단하게 위 이미지와 같이 4개의 파트로 나눌 수 있습니다. 여기서 Message Body에 들어가는 데이터 타입을 HTTP Header에 명시해줄 수 있다. 이 때 명시할 수 있도록 해주는 필드가 바로 Content-type이다. 추가적으로 Content-type 필드에 MIME(Multipurpose Internet Mail Extensions) 타입을 기술해줄 수 있는데, 여러 타입 중 하나가 바로 multipart 인 것이다.

 

< MIME에서의 multipart & multipart/form-data >

multipart 타입을 통해 MIME은 트리 구조의 메세지 형식을 정의할 수 있다.

  • [ Multipart 메세지 ]
    • 서로 붙어있는 여러 개의 메세지를 포함하여 하나의 복합 메세지로 보내진다.
    • MIME multipart 메세지는 “Content-type:” 헤더에 boundary 파라미터를 포함함.
    • boundary는 메세지 파트를 구분하는 역할을 하며, 메세지의 시작과 끝 부분도 나타낸다.
    • 첫번째 Boundary 전에 나오는 내용은 MIME을 지원하지 않는 클라이언트를 위해 제공된다.
    • boundary 를 선택하는 것은 클라이언트의 몫입니다. 보통 무작위의 문자를 선택해 메세지의 본문과 충돌을 피함. Ex) UUID
    • HTTP form을 채워서 제출하면, 가변 길이 텍스트 필드와 업로드 될 객체는 각각 멀티파트 본문을 구성하는 하나의 파트가 되어 보내진다. 멀티 파트 분몬은 여러 다른 종류와 길이의 값으로 채워진 form을 허용함.
    • multipart/form-data: 사용자가 양식을 작성한 결과 값의 집합을 번들로 만드는데 사용한다.

(출처: 기록은 기억을 이긴다)

 

<파일 업로드할 때 알아야하는 HTTP 규약>

(이미지 출처: 탁구치는 개발자)

서버에 multipart/form-data로 데이터를 보낼때의 request header와 body는 위 이미지와 같이 구성되어있다.

위 이미지과 함께 다음과 같은 HTTP 통신 규격을 확인해 볼 수 있다.

  1. Content-Type가 multipart/form-data로 지정 되어있어야 서버에서 정상적으로 데이터를 처리할 수 있다.
  2. 전송되는 파일 데이터의 구분자로 boundary에 지정되어 있는 문자열을 이용한다.
  3. boundary의 문자열 중 마지막 **------WebKitFormBoundaryQGvWeNAiOE4g2VM5--** 값은 다른 값과 다르게 --가 마지막에 붙었는데, -- 는 body의 끝을 알리는 의미를 가진다.

 

Spring boot 3.0에서 MultipartResolver bean 생성

스프링부트 3.0 부터는 multipart 수행을 담당하는 MultipartResolver가 StandardServletMultipartResolver 이 기본으로 변경된다. 따라서 아래와 같이 Bean등록을 수행하여 multipart/form 요청에 대한 처리 모듈을 등록한다.

 

@Configuration
public class MultipartConfig {

    @Value("${file.multipart.maxUploadSize:10485760}")
    private long maxUploadSize;

    @Value("${file.multipart.maxUploadSizePerFile:10485760}")
    private long maxUploadSizePerFile;

    @Bean
    public MultipartResolver multipartResolver() {
        StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
        return multipartResolver;
    }

    @Bean
    public MultipartConfigElement multipartConfigElement() {
        MultipartConfigFactory factory = new MultipartConfigFactory();
        factory.setMaxRequestSize(DataSize.ofBytes(maxUploadSize));
        factory.setMaxFileSize(DataSize.ofBytes(maxUploadSizePerFile));

        return factory.createMultipartConfig();
    }
}

 

파일 업로드 용량 제한을 위해서 MultipartConfigElement bean도 생성을 한다. MultipartConfigFactory를 활용해 최대 전송 사이즈를 설정한다.

 

Multipart 업로드 Rest api 작성

Multipart 처리를 위해서는 보통 Post 메소드 형식을 사용하고 MultipartFile은 RequestParam형식으로 받는다.

MultipartFile이 제대로 들어오는지 확인하기 위해 아래 예제 Api를 만들어 본다.

@RestController
@RequestMapping("/api/v1/test")
public class TicketController {

    @RequestMapping(value = "/tickets/pic", method = RequestMethod.POST, produces = "application/json", consumes = "multipart/form-data")
    public ResponseEntity<TicketResponseDTO> addSeasonTicketPicture(
            @Parameter(name = "membershipUid", description = "멤버십UID", in = ParameterIn.QUERY)
            @RequestParam(value = "membershipUid") String membershipUid,
            @Parameter(name = "file", description = "업로드 사진 데이터")
            @RequestParam(value = "file") MultipartFile file) throws Exception{

        TicketResponseDTO result = TicketResponseDTO.builder().membershipUid(membershipUid).build();

        return new ResponseEntity<>(result, HttpStatus.OK);
    }
}

 

MockMvc를 이용해 JUnit 테스트 코드에서 MultipartFile 전송 확인하기

 

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = CommonApplication.class)
@TestPropertySource(properties = {"spring.profiles.active=unittest"})
@DirtiesContext
public class TicketControllerTest {

    @Autowired
    private WebApplicationContext context;

    private MockMvc mockMvc;

    @Before
    public void prepare() throws Exception {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
                .addFilters(new CharacterEncodingFilter("UTF-8", true)).build();
    }

    @After
    public void tearDown() throws Exception {

    }

    @Test
    public void whenAddSeasonTicketPicture__thenExecuteCorrectly() throws Exception {
        ClassPathResource classPathResource = new ClassPathResource("/testdata/test.jpg");

        MvcResult result = mockMvc.perform(multipart("/api/v1/test/tickets/pic")
                        .file(new MockMultipartFile("file", "test.jpg", "image/jpeg", classPathResource.getInputStream()))
                        .param("membershipUid", "mem_testid")
                        .contentType("multipart/form-data"))
                .andExpect(status().isOk())
                .andReturn();

        TicketResponseDTO response = new ObjectMapper().readValue(result.getResponse().getContentAsString(),
                new TypeReference<TicketResponseDTO>() {
                });

        assertThat(response.getMembershipUid()).isEqualTo("mem_testid");
    }
}

앞서 작성한 api를 테스트 할수 있는 테스트 코드를 작성하였다. 웹 Front 단 화면없이 backend 테스트를 바로하기 위해  MockMultipartFile을 활용해 multipart 요청을 만들어 본다.

 

new MockMultipartFile("file", "test.jpg", "image/jpeg", classPathResource.getInputStream())

여기서 제일앞의 인자는 실제 api에서 받을 param을 나타내는 값으로 "file"로 지정했으면 rest api request param 명도 동일하게 file로 맞추어야 한다.  다르게 된다면 api단에서 null로 multipart file을 받게 된다.

 

테스트 코드를 실행하고 api단 break를 걸어보면 file 인자가 제대로 들어온 것을 볼 수 있다. 

 

MultipartFile 업로드 용량 제한 확인

앞서 MultipartConfigElement bean 등록을 통해 max file size limit를 설정할 수 있다고 하였다. 테스트로 maxUploadSize를 작은값으로 설정하고 실제로 over되는 파일을 업로드 했을때 limit 제한이 정상동작하는지를 swagger-ui 로 확인해 본다.

 

 아래 빈 설정에서 maxUploadSize를 58 byte로 설정을 하였다.

@Bean
public MultipartConfigElement multipartConfigElement() {
    MultipartConfigFactory factory = new MultipartConfigFactory();
    factory.setMaxRequestSize(DataSize.ofBytes(maxUploadSize));
    factory.setMaxFileSize(DataSize.ofBytes(maxUploadSizePerFile));

    return factory.createMultipartConfig();
}

 

Swagger-ui 설정이 되어있다면 작성한 Api 를 아래 swagger-ui에서 테스트 해볼 수 있다. 2MB 로 위 설정값 58 byte보다 큰 테스트 이미지를 업로드 하였더니 아래와 같이 "Maximum upload size exceeded" 응답이 오는것을 볼 수 있다. Spring boot에서 용량 제한 설정이 잘 동작 함을 알 수 있다.

 

 

-- The End --