포스트

Multipartfile과 dto함께 요청하기

문제상황

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import springboot.yongjunstore.request.RoomPostDto;
import springboot.yongjunstore.service.RoomPostService;

@RestController
@RequiredArgsConstructor
@RequestMapping("/roomPost")
public class RoomPostController {

    private final RoomPostService roomPostService;

    @PostMapping("/create")
    public ResponseEntity roomCreate(@Valid @RequestBody RoomPostRequest roomPostRequest, @RequestParam("uploadFiles") MultipartFile[] uploadFiles){

        roomPostService.createRoom(roomPostDto, uploadFiles);

        return ResponseEntity.status(HttpStatus.OK).build();
    }
}


보통 컨트롤러에서 MultipartFile 요청을 할 때는 @RequestParam을 사용했다.
Dto를 요청할 때는 @RequestBody를 주로 사용했고, 잘 될 것만 같았는데 헤더에 토큰을 넣었는데도 이상하게 토큰이 null을 뱉었다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import springboot.yongjunstore.config.jwt.JwtProvider;

import java.io.IOException;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

    private final JwtProvider jwtProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        // 1. Request Header 에서 JWT 토큰 추출
        String accessToken = jwtProvider.resolveToken((HttpServletRequest) request);

        // 2. validateToken 으로 토큰 유효성 검사
        if (accessToken != null && jwtProvider.validateToken(accessToken)) {
            // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장
            Authentication authentication = jwtProvider.getAuthentication(accessToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }
}

왜 이러지?
혹시나 해서 다른 컨트롤러를 사용해 보니.. 정상적으로 통과 되는 것을 보니 토큰 문제는 아니었다.
에러 메세지

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.web.multipart.MultipartException: Current request is not a multipart request] with root cause

해석해 보면, 현재 요청이 멀티파트 요청이 아닌데 멀티파트 요청으로 처리하려고 시도하고 있기 때문에 발생한 것

하지만 분명히 헤더에 명시했다.

1
2
3
4
5
6
      axios.post(import.meta.env.VITE_APP_API_URL + '/roomPost/create', this.roomPostDto, formData, {
  headers: {
    'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
    'Content-Type': 'multipart/form-data'
  }
})


‘Content-Type’: ‘multipart/form-data’ <– 근데 이거 재데로 적용이 된게 맞나??

생각해 보니 헤더에 명시 했는데도 request에서 헤더 값을 전부 null로 리턴했다. 뭔가 방식이 잘 못 됐다는 걸 느꼈다..


그래서 다시 생각해 봤다.
@RequestBody : HTTP 요청의 body를 자바 객체로 변환
@RequestParam : HTTP 요청의 쿼리 파라미터(query parameter)를 매개변수 사용
같이 못 쓸 이유가 없다.



의심 되는 상황


  1. Content-Type@RequestBody를 json으로 변환 시켜야 해서 application/json으로 고정 되는 건가?
  2. multipart/form-dataContent-Type이 잡혀서 json으로 변환을 못시키는 건가?
  3. MultipartFile[]은 단독으로 사용해야만 하는 건가?


결론은 multipart/form-data를 사용하는 것으로 헤더에 넘겼기 때문에 json으로 처리하지 못했고, Dto 또한 마찬가지 였던 것 이었다.
그 예로, multipart/form-dataapplication/json으로 헤더에 입력하면


에러 메세지

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.web.multipart.MultipartException: Current request is not a multipart request] with root cause

요약하자면 결국, 멀티파트로 요청해야 하는데 현재 요청이 멀티파트 요청이 아니다.


문제 파악


multipart/form-data 요청 시 : 현재 요청이 멀티파트 요청이 아닌데 멀티파트 요청으로 처리하려고 시도하고 있기 때문에 발생한 것

application/json 요청 시 : 멀티파트로 요청해야 하는데 현재 요청이 멀티파트 요청이 아니다.


하나의 Content-Type으로 서로 다른 타입의 객체를 처리하려고 해서 발생한 문제였다…
천천히 생각해 보면 매우 간단한 문제였던 것..




또 다른 문제


@RequestPart 사용


@RequestPart를 사용해서 처리하는 방법을 생각했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import springboot.yongjunstore.request.RoomPostRequest;
import springboot.yongjunstore.service.RoomPostService;

import java.util.List;

@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/roomPost")
public class RoomPostController {

    private final RoomPostService roomPostService;

    @PostMapping(value = "/create")
    public ResponseEntity roomCreate(@Valid @RequestPart(value = "roomPostRequest", required = false)RoomPostRequest roomPostRequest, 
                                            @RequestPart(value = "uploadImages", required = false) List<MultipartFile> uploadImages){
        
        roomPostService.createRoom(roomPostRequest, uploadImages);

        return ResponseEntity.status(HttpStatus.OK).build();
    }
}

이미지 파일과 RoomPostRequest를 formData에 넣어서 요청하기로 했다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const formData = new FormData();

// roomPost Json을 formData에 담기
formData.append('roomPostRequest', JSON.stringify(this.roomPostRequest));

// 업로드된 이미지 uploadImages 담기.
for (let i = 0; i < this.files.length; i++) {
  formData.append('uploadImages', this.files[i]);
}


axios.post(import.meta.env.VITE_APP_API_URL + '/roomPost/create', formData, {
  headers: {
    'Authorization': `Bearer ${this.token}`,
    'Content-Type': 'multipart/form-data',
    //'Content-Type': 'application/json'
  }
})


이렇게 하면 정상적으로 처리될 줄… 알았다.

1
2
3
error : "Unsupported Media Type"
path : "/roomPost/create"
status : 415

Unsupported Media Type : 지원되지 않는 형식으로 요청을 해서 요청에 대한 승인을 거부


해당 415와 400을 번갈아 가며 만나고 많은 시간을 써가며 해결하려 했지만 방법을 찾기 못해서 다른 해결 방안을 찾았다.



해결

바로 @ModelAttribute를 사용해서 해결하는 방법!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import springboot.yongjunstore.request.RoomPostRequest;
import springboot.yongjunstore.service.RoomPostService;
import java.util.List;

@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/roomPost")
public class RoomPostController {

    private final RoomPostService roomPostService;

    @PostMapping(value = "/create")
    public ResponseEntity roomCreate(@Valid @ModelAttribute RoomPostRequest roomPostRequest,
                                            @RequestPart(value = "uploadImages") List<MultipartFile> uploadImages){

        roomPostService.createRoom(roomPostRequest, uploadImages);

        return ResponseEntity.status(HttpStatus.OK).build();
    }
}

요청 타입은 프론트에서 fromData 그대로 받아주고 RoomPostRequest@ModelAttribute로 해 주었다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const formData = new FormData();

// roomPostDto를 JSON으로 미리 변환해서 formData에 넣는다.
for (const key in this.roomPostRequest) {
  if (Object.prototype.hasOwnProperty.call(this.roomPostRequest, key)) {
    formData.append(key, this.roomPostRequest[key]);
    console.log(this.roomPostRequest[key]);
  }
}

// 업로드된 이미지 uploadImages 담기.
for (let i = 0; i < this.files.length; i++) {
  formData.append('uploadImages', this.files[i]);
}


axios.post(import.meta.env.VITE_APP_API_URL + '/roomPost/create', formData, {
  headers: {
    'Authorization': `Bearer ${this.token}`,
    'Content-Type': 'multipart/form-data',
    //'Content-Type': 'application/json'
  }
})


대신 key, value 형태로 한땀 한땀 반복문을 사용해 formData에 추가해 주었다.


정상요청

정상적으로 요청되었고, 값도 잘 저장되는 것을 확인했다 하지만 @ModelAttribute를 사용하지 않고 @RequestPart로 해결하고 싶은 마음이 여전히 남아 있어 찝찝한 느낌이 남은 해결이었다.


리펙토링을 하게된다면 다시 한번 해당 코드를 수정해야 될 것 같다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.