[Spring] @RequestPart를 이용한 MultipartFile, DTO 처리 및 테스트

오형상·2023년 6월 25일
2

RecipeFriend

목록 보기
1/8
post-thumbnail

문제 - @RequestParam + @RequestBody

쇼핑몰 토이 프로젝트 중 해당 품목의 이미지를 첨부하고 품목 정보를 입력해 아이템을 등록하는 기능을 구현하려고 MultipartFile은 @RequestParam으로 Dto는 @RequestBody를 사용해서 받을려고 했지만 아래의 오류가 발생했다.

2023-06-28T21:17:30.208+09:00  WARN 7120 --- [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content-Type 'multipart/form-data;boundary=--------------------------150556874451848119877768;charset=UTF-8' is not supported]

오류를 해결하고자 구글링을 하였고 그 결과 MultipartFile과 Dto를 함께 요청 받으려면 두개 모두 @RequestPart를 이용해야 한다.
❗ 해당 방법을 사용하면 Swaager로 테스트가 불가능하여 Postman을 사용하였습니다.

해결 - @ReqeustPart + @ReqeustPart

Controller

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/items")
public class ItemController {

    private final ItemService itemService;

    @Tag(name = "Item", description = "품목 API")
    @Operation(summary = "품목 추가")
    @PostMapping
    public Response<ItemDto> createItem(@RequestPart ItemCreateRequest request, @RequestPart MultipartFile multipartFile) {
        ItemDto response = itemService.saveItem(request, multipartFile);

        return Response.success(response);
    }
}

Service

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class ItemService {

    private final ItemRepository itemRepository;

    private final BrandRepository brandRepository;

    private final AwsS3Service awsS3Service;

    public ItemDto saveItem(ItemCreateRequest request, MultipartFile multipartFile) {

        itemRepository.findItemByItemName(request.getItemName())
                .ifPresent((item -> {
                    throw new AppException(DUPLICATE_ITEM, DUPLICATE_ITEM.getMessage());
                }));

        Brand findBrand = getBrand(request.getBrandName());

        String originImageUrl = awsS3Service.uploadBrandOriginImage(multipartFile);

        request.setItemPhotoUrl(originImageUrl);

        Item savedItem = itemRepository.save(request.toEntity(findBrand));

        return savedItem.toItemDto();

    }
    
	// 브랜드 이름을 통해 해당 브랜드를 찾는 메소드
    private Brand getBrand(String brandName) {
        Brand findBrand = brandRepository.findBrandByName(brandName)
                .orElseThrow(() -> new AppException(BRAND_NOT_FOUND, BRAND_NOT_FOUND.getMessage()));
        return findBrand;
    }
}

단위 테스트

@WebMvcTest(ItemController.class)
class ItemControllerTest {

    @MockBean
    ItemService itemService;

    @Autowired
    ObjectMapper objectMapper;

    @Autowired
    private MockMvc mockMvc;


    @Test
    @DisplayName("브랜드 등록 성공")
    @WithMockCustomUser(role = CustomerRole.ROLE_ADMIN)
    public void create_brand_success() throws Exception {

        // given
        ItemCreateRequest request = ItemCreateRequest.builder()
                .itemName("testItem")
                .price(21000)
                .stock(1000)
                .brandName("testBrand")
                .itemPhotoUrl("test")
                .build();

        String valueAsString = objectMapper.writeValueAsString(request);

        final String fileName = "testImage1"; 
        final String contentType = "png"; 
        MockMultipartFile multipartFile = setMockMultipartFile(fileName, contentType);

        Brand findBrand = Brand.builder()
                .id(1L)
                .name("testBrand")
                .originImagePath("s3/brand/url")
                .build();

        findBrand.setCreatedDate(LocalDateTime.now());
        findBrand.setLastModifiedDate(LocalDateTime.now());

        ItemDto response = ItemDto.builder()
                .itemName("testItem")
                .stock(1000)
                .price(21000)
                .itemPhotoUrl("s3/item/url")
                .brand(findBrand)
                .build();


        given(itemService.saveItem(any(ItemCreateRequest.class), any(MockMultipartFile.class)))
                .willReturn(response);

        // when & then
        mockMvc.perform(multipart("/api/v1/items")
                        .file(new MockMultipartFile("request", "", "application/json", valueAsString.getBytes(StandardCharsets.UTF_8)))
                        .file(multipartFile)
                        .contentType(MULTIPART_FORM_DATA)
                        .accept(APPLICATION_JSON)
                        .characterEncoding("UTF-8")
                        .with(csrf()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.resultCode").value("SUCCESS"))
                .andExpect(jsonPath("$.result.itemName").value(response.getItemName()))
                .andExpect(jsonPath("$.result.price").value(response.getPrice()))
                .andExpect(jsonPath("$.result.stock").value(response.getStock()))
                .andExpect(jsonPath("$.result.itemPhotoUrl").value(response.getItemPhotoUrl()))
                .andExpect(jsonPath("$.result.brand.createdDate").exists())
                .andExpect(jsonPath("$.result.brand.deletedDate").doesNotExist())
                .andExpect(jsonPath("$.result.brand.lastModifiedDate").exists())
                .andExpect(jsonPath("$.result.brand.id").value(response.getBrand().getId()))
                .andExpect(jsonPath("$.result.brand.name").value(response.getBrand().getName()))
                .andExpect(jsonPath("$.result.brand.originImagePath").value(response.getBrand().getOriginImagePath()))
                .andDo(print());
    }

    private MockMultipartFile setMockMultipartFile(String fileName, String contentType) {
        return new MockMultipartFile("multipartFile", fileName + "." + contentType, contentType, "<<data>>".getBytes());
    }

}

Postman 테스트

Body가 form-data인지 dto값은 JSON 형식인지 등 세부사항들도 정확히 입력해야 한다.


전체코드 보러가기

1개의 댓글

comment-user-thumbnail
2023년 12월 21일

같은 문제에 직면했는데 덕분에 해결했습니다 감사합니다~

답글 달기