[Spring Boot] JSON 데이터와 Multipart 파일 함께 처리 시 Current request is not a multipart request 에러
문제 상황
유저 정보 수정 시, 클라이언트에서는 프로필 사진과 회원 정보 변경을 함께 요청한다.
multipart/form-data 방식으로 요청하던 기존 코드에서 json 데이터로 요청하도록 리팩토링 후 문제가 발생했다!
문제 상황 관련 코드
기존 문제 없던 코드
HTML
파일 업로드를 위해 form에서 enctype="multipart/form-data"으로 설정했다.
<div class="main-content">
<div class="profile-container">
<div class="profile-pic">
<img id="profileImageEditPreview" th:src="@{${user.filepath != null ? user.filepath : '/files/user.png'}}" alt="프로필 사진">
</div>
</div>
<div class="form-container">
<form th:action="@{/my/profile}" th:object="${user}" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="profileImage">프로필 사진</label>
<input type="file" id="profileImage" name="profileImage" />
</div>
<div class="form-group">
<label for="name">이름</label>
<input type="text" id="name" name="name" th:field="*{name}" readonly/>
</div>
<div class="form-group">
<label for="usernick">닉네임</label>
<input type="text" id="usernick" name="usernick" th:field="*{usernick}"/>
</div>
<div class="form-group">
<label for="phoneNumber">연락처</label>
<input type="tel" id="phoneNumber" name="phoneNumber" th:field="*{phoneNumber}" />
</div>
<div class="button-group">
<a href="/my" class="link-button light-button">돌아가기</a>
<button type="submit" id="submit-button" class="save-button">저장</button>
</div>
<input type="hidden" id="userId" th:value="${user.id}"/>
</form>
</div>
</div>
Controller
@ModelAttribute를 사용해 폼에서 일반 텍스트인 name, usernick, phoneNumber를 DTO인 UserEditRequest로 매핑하고,
@RequestParam을 사용해 파일을 받아서 처리한다.
@PostMapping("/my/profile")
public String userEdit(HttpServletRequest request,
@ModelAttribute UserEditRequest userEditDto,
@RequestParam("profileImage") MultipartFile profileImage,
Model model) {
....
}
이렇게 했을 땐 문제 없이 잘 작동했지만, 컨트롤러를 RESTful하게 리팩토링한 후 문제가 발생했다.
변경 후 문제가 발생한 코드
Controller
컨트롤러를 PutMapping으로 바꿨고, HTML 페이지를 리턴하는게 아닌 ResponseEntity를 응답하도록 바꿨다.
또, ajax로 보낸 JSON 데이터를 @RequestBody로 받고, 파일은 @RequestParam으로 받으려고 했다.
@PutMapping("/api/users/profile")
@ResponseBody
public ResponseEntity<String> updateUserProfile(HttpServletRequest request,
@RequestBody UserEditRequest userEditDto,
@RequestParam("profileImage") MultipartFile profileImage) {
}
js
usernick과 phoneNumber를 userData에 담아 JSON 타입으로 요청을 보냈다.
$(document).ready(function() {
$('#profile-edit-form').on('submit', function(e) {
e.preventDefault();
const userData = {
usernick: $('#usernick').val(),
phoneNumber: $('#phoneNumber').val()
};
$.ajax({
type: 'PUT',
url: '/api/users/profile',
contentType: 'application/json',
data: JSON.stringify(userData),
success: function(response) {
alert(response);
window.location.href = '/my/profile';
},
error: function(error) {
alert('회원정보 수정 중 오류가 발생했습니다.');
}
});
});
변경 후 아래 에러가 발생했다.
해결 방법
@RequestBody는 JSON 데이터를 바인딩하고, 파일 데이터는 처리할 수 없다.
@RequestParam은 multipart/form-data 요청에서 파일 데이터를 처리할 수 있다.
하나의 요청에서 일반 텍스트 데이터는 json 이니까 @RequestBody로 처리되고, 파일은 multipart/form-data 이니까 @RequestParam으로 처리될거라 생각했지만 아니다!
요청 전체가 application/json 형식으로 들어오는 상황이고, 파일 데이터는 요청 본문에 포함되지 않아서 아예 서버에 전달되지 않는다.
즉, 다른 방법으로 json 형식인 DTO와 파일 데이터를 함께 처리할 수 있도록 해야하는데,
찾아보니 @RequestPart로 함께 처리할 수 있었다!
@RequestPart를 사용하려면 multipart/form-data 형식으로 요청을 보내야한다.
1. 서버로 보낼 formData 객체를 하나 생성
2. userEditDto 객체를 생성해 사용자가 입력한 값으로 필드를 채운 뒤, json 문자열로 만들어서 'userEditDto'라는 이름으로 formData 객체에 넣음
3. 파일 데이터를 formData 객체에 넣음
4. formData를 서버로 전송
5. 서버에서는 @RequestPart로 받아서 JSON 데이터를 자동으로 DTO 객체로 매핑해주고, profileImage는 MultipartFile로 처리한다.
$(document).ready(function() {
$('#profile-edit-form').on('submit', function(e) {
e.preventDefault();
// FormData 객체 생성
const formData = new FormData();
// userEditDto를 생성하고 필드 값을 추가
const userEditDto = {
usernick: $('#usernick').val(),
phoneNumber: $('#phoneNumber').val()
};
// userEditDto를 JSON 문자열로 변환하여 FormData에 추가
formData.append('userEditDto',
new Blob([JSON.stringify(userEditDto)],
{type: "application/json"}));
// 프로필 이미지 파일을 FormData에 추가
const profileImage = $('#profileImage')[0].files[0];
if (profileImage) {
formData.append('profileImage', profileImage);
}
$.ajax({
type: 'PUT',
url: '/api/users/profile',
data: formData,
contentType: false, // 파일 전송 시 반드시 false로 설정
processData: false, // jQuery가 데이터를 처리하지 않도록 설정
success: function(response) {
alert(response);
window.location.href = '/my/profile';
},
error: function(jqXHR, textStatus, errorThrown) {
alert('회원정보 수정 중 오류가 발생했습니다.');
}
});
});
@PutMapping("/api/users/profile")
@ResponseBody
public ResponseEntity<String> updateUserProfile(HttpServletRequest request,
@RequestPart(value = "userEditDto") UserEditRequest userEditDto,
@RequestPart(value = "profileImage", required = false) MultipartFile profileImage) {
}
요렇게 고치니 정상적으로 동작한다!