[Spring Boot] @RequestParam vs @RequestPart

2022. 3. 8. 22:52Spring/Boot

조사하게 된 원인

프로젝트를 하면서 Multipart 데이터로 JSON과 이미지 파일 데이터를 함께 받기 위해 스프링 기능을 찾던 도중 @RequestPart에 대해 새로 알게 되었다. @RequestParam이라는 어노테이션도 있는데 둘의 차이에 대해 조사해 보았다. 이는 공식 문서에 잘 나타나 있다. 먼저 각 어노테이션에 대한 공식 문서의 설명을 보자.

@RequestParam

Annotation which indicates that a method parameter should be bound to a web request parameter.
Supported for annotated handler methods in Spring MVC as follows:
In Spring MVC, "request parameters" map to query parameters, form data, and parts in multipart requests. This is because the Servlet API combines query parameters and form data into a single map called "parameters", and that includes automatic parsing of the request body.
If the method parameter type is Map and a request parameter name is specified, then the request parameter value is converted to a Map assuming an appropriate conversion strategy is available.
If the method parameter is Map<String, String> or MultiValueMap<String, String> and a parameter name is not specified, then the map parameter is populated with all request parameter names and values.

메소드 파라미터가 웹 요청 파라미터에 바인딩되어야 함을 나타내는 어노테이션이다. 다음과 같이 Spring MVC의 어노테이션 핸들러 메소드에 대해 지원한다.
Spring MVC에서 요청 파라미터는 쿼리 파라미터, form 데이터, Multipart 요청의 파트에 매핑된다. 이는 서블릿 API가 쿼리 파라미터와 form 데이터를 파라미터라는 하나의 맵으로 결합하고 여기에는 request body의 자동 파싱이 포함되기 때문이다.
메소드 파라미터 타입이 Map이고 파라미터 이름이 명시된 경우 적절한 변환 전략을 사용할 수 있다고 가정하고 요청 파라미터 값은 Map으로 변환된다. 메소드 파라미터가 Map<String, String> 또는 MultiValueMap<String, String>이고 파라미터 이름이 명시되지 않은 경우 map 파라미터는 모든 요청 파라미터 이름과 값으로 채워진다.

@RequestPart

Annotation that can be used to associate the part of a "multipart/form-data" request with a method argument.
Supported method argument types include MultipartFile in conjunction with Spring's MultipartResolver abstraction, javax.servlet.http.Part in conjunction with Servlet 3.0 multipart requests, or otherwise for any other method argument, the content of the part is passed through an HttpMessageConverter taking into consideration the 'Content-Type' header of the request part. This is analogous to what @RequestBody does to resolve an argument based on the content of a non-multipart regular request.

“multipart/form-data” 요청의 일부를 메소드 인수와 연결하는 데 사용할 수 있는 어노테이션이다.
지원되는 메소드 인수 타입에는 Spring의 MultipartResolver 추상화와 결합된 MultipartFile, Servlet 3.0 multipart 요청과 결합된 javax.servlet.http.Part가 포함되고, 다른 메소드 인수의 경우 해당 파트의 내용이 요청 헤더의 Content-Type을 고려하여 HttpMessageConverter를 통해 전달된다. 이는 @RequestBody가 Multipart가 아닌 일반 요청의 내용을 기반으로 인수를 처리하기 위해 수행하는 작업과 유사하다.

 

두 설명만 보면 @RequestPart는 multipart/form-data에 특화된 어노테이션이라는 것을 알 수 있다. 둘의 차이점을 정확히 알기 위해 먼저 공식 문서의 글을 살펴보자.

Note that @RequestParam annotation can also be used to associate the part of a "multipart/form-data" request with a method argument supporting the same method argument types. The main difference is that when the method argument is not a String or raw MultipartFile / Part, @RequestParam relies on type conversion via a registered Converter or PropertyEditor while RequestPart relies on HttpMessageConverters taking into consideration the 'Content-Type' header of the request part. RequestParam is likely to be used with name-value form fields while RequestPart is likely to be used with parts containing more complex content e.g. JSON, XML).

@RequestParam 어노테이션은 “multipart/form-data” 요청의 일부를 같은 메소드 인수 타입을 지원하는 메소드 인수와 연결하는 데 사용할 수 있다. 주요 차이점은 메소드 인수가 String 또는 원시 MutipartFile/Part가 아닌 경우 @RequestParam은 등록된 Converter 또는 PropertyEditor를 통한 형식 변환에 의존하는 반면 @RequestPart는 요청 부분의 ‘Content-Type’ 헤더를 고려하는 HttpMessageConverters에 의존한다. @RequestParam은 이름-값 form 필드와 함께 사용되는 반면 @RequestPart는 더 복잡한 내용(JSON, XML 등)을 포함하는 부분과 함께 사용된다.

분석

1. 메소드 인수가 String 또는 MultipartFile/Part가 아닌 경우

  • @RequestParam
    • 등록된 Converter 또는 PropertyEditor로 변환을 시도한다.
  • @RequestPart
    • 요청 헤더의 Content-Type에 해당하는 HttpMessageConverter로 변환을 시도한다.
💡스프링은 데이터 바인딩을 위해 Converter, PropertyEditor, Formatter를 제공한다. @RequestParam은 이를 참조해 데이터를 변환하는 것이다. 이 글에서 이 내용은 다루지 않는다.

2. 사용되는 곳

  • @RequestParam
    • name-value 쌍의 form 필드와 함께 사용된다.
  • @RequestPart
    • JSON, XML등을 포함하는 복잡한 내용의 Part와 함께 사용된다.

예제

필자가 작성한 API는 다음과 같다.

@PostMapping
    fun signUp(
        @RequestPart("user-data") userForm: UserForm,
        @RequestPart("file-data") image: MultipartFile
    ): ResponseEntity<UserSignUpResponseDto> {
        val storeFile: UploadFile =
            fileUtil.storeFile(image) ?: throw IllegalStateException("Image is not submitted.")

        uploadFileService.save(storeFile)

        val userRequestDto = with(userForm) {
            UserSignUpRequestDto(
                email = email,
                username = username,
                password = password,
                image = storeFile
            )
        }

        val savedUser = userService.save(userRequestDto)
        return ResponseEntity.ok(UserSignUpResponseDto.of(savedUser))
    }
data class UserForm(
    var email: String,
    var password: String,
    var username: String,
)

이를 통해 예측해볼 수 있는 것은 다음과 같다.

  • 클라이언트에서 multipart/form-data로 UserForm 클래스에 맞는 JSON 데이터와 이미지 파일을 함께 전송해야 한다는 것
  • UserForm은 String 또는 MultipartFile/Part가 아니고 @RequestPart를 사용했기 때문에 요청 헤더의 Content-Type에 해당하는 HttpMessageConverter를 통해 변환될 것이라는 것 
    • MappingJacksonHttpMessageConverter에 의해 JSON 데이터가 UserForm 형태로 변환될 것이라는 것을 알 수 있다.

 

테스트 코드는 다음과 같다.

package wscrg.exposeuback.api

import com.fasterxml.jackson.databind.ObjectMapper
import org.junit.jupiter.api.Test
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.mock.web.MockMultipartFile
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import wscrg.exposeuback.domain.dto.user.UserForm

@SpringBootTest
@AutoConfigureMockMvc
internal class UserApiControllerTest private constructor(
    @Autowired
    val mockMvc: MockMvc,
    @Autowired
    val objectMapper: ObjectMapper
) {
    companion object {
        val log = LoggerFactory.getLogger(MockMvcResultMatchers::class.java)
    }

    @Test
    fun user_and_image_동시_업로드_테스트() {
        //given
        val imageFile = MockMultipartFile(
            "file-data",
            "image.jpeg",
            MediaType.IMAGE_JPEG_VALUE,
            "<<image jpeg>>".toByteArray()
        )

        val content =
            objectMapper.writeValueAsString(UserForm(email = "abcdefu@google.com", username = "jiji", password = "1234"))

        val json = MockMultipartFile(
            "user-data",
            "jsonData",
            MediaType.APPLICATION_JSON_VALUE,
            content.toByteArray()
        )

        //when
        val requestBuilder = multipart("/api/users")
            .file(imageFile)
            .file(json)
            .contentType(MediaType.MULTIPART_MIXED_VALUE)
            .characterEncoding("UTF-8")

        val perform = mockMvc.perform(requestBuilder)

        //then
        val result = perform.andExpect(status().isOk).andReturn()
        val response = result.response
        log.info("response content-type: {}", response.contentType)
        log.info("response content: {}", response.contentAsString)
    }
}

결론

@RequestParam

  • 쿼리 파라미터, 폼 데이터, Multipart 등 많은 요청 파라미터를 처리할 수 있는 어노테이션이다.
  • 메소드 파라미터 타입이 String 또는 MultipartFile/Part가 아닌 경우 Converter 또는 PropertyEditor를 참조해 변환을 시도한다.

@RequestPart

  • multipart/form-data에 특화되어 여러 복잡한 값을 처리할 때 사용할 수 있는 어노테이션이다.
    • 위와 같이 한 번에 사용자 정보 및 파일을 함께 처리하는 경우 사용할 수 있다.
  • 메소드 파라미터 타입이 String 또는 MultipartFile/Part가 아닌 경우 요청 헤더의 Content-Type을 참조하여 해당하는 HttpMessageConveter로 변환을 시도한다.

배운 점

  • @RequestParam과 @RequestPart의 차이에 대해 알게 되었다.
  • 파일(바이너리 데이터)과 JSON 데이터를 함께 처리하는 방법에 대해 알게 되었다.
  • 스프링 부트에서 Multipart 데이터에 대해 Mocking을 활용하여 테스트하는 방법에 대해 알게 되었다.
  • HttpMessageConveter에 대해 복습하게 되었다.