티스토리 뷰
해당 이슈는 Postman 같은 똑똑한 클라이언트를 사용하지 않고
Windows or Mac 등 직접 파일 업로드 요청을 보낼 때 발생할 수 있는 문제다
내 경우엔 Windows 클라이언트가 C++로 짠 코드로 서버에 HTTP 요청을 보내 파일 업로드를 요청했다
파일 업로드 시 파일 이름이 한글인 경우 업로드된 파일 이름이 깨지는 상황이 발생했다
Windows 클라이언트 개발자에게 HTTP 요청을 전달받아 상황을 재현했다
업로드할 파일이 한글 파일명을 가지고 있었기에 아래와 같은 형태로 요청을 만들었다고 한다
###
POST http://localhost:8080/test/file-upload
Accept: application/json
Content-Type: multipart/form-data; boundary=---------------------------7d13a23b368
-----------------------------7d13a23b368
Content-Disposition: form-data; name="file"; filename="헬로월드.txt"
1234
-----------------------------7d13a23b368--
이 요청의 파일 이름으로 서버 스토리지에 저장하는데 ????. txt가 저장 됐다
기존에 프로젝트가 파일 업로드 기능을 멀쩡히 잘 쓰고 있었고 다국어 지원도 해야 해서
한글은 물론 일본어, 중국어까지 지원하는 상태였기에 의아했다
급하게 고쳐야 하는 이슈였기 때문에 상세한 원인파악보다 선해결 후파악하기로 하고
클라이언트가 filename을 Base64 인코딩하여 요청하고 서버에서 다시 Base64 디코딩하여 사용하도록 수정했다
이슈 해결은 잘 됐으니 상세한 원인 파악을 해보자
우선 파일 업로드를 테스트할 간단한 컨트롤러 코드를 작성한다
파일의 실제 이름을 가져오기 위해 MultipartFile#getOriginalFilename 메서드를 사용하였다
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
public class FileUploadController {
@PostMapping("/test/file-upload")
public String upload(@RequestPart MultipartFile file) {
return file.getOriginalFilename();
}
}
HTTP 요청이 어떻게 나가는지 보기 위해 wireshark 또는 fiddler도 준비하면 좋다
mac + wireshark 기준으로 설명하지만 사용법은 크게 다르지 않으니 각자 OS & 입맛에 맞게 설치해 주도록 하자
wireshark에서 패킷 캡처를 위해 wireshark 실행 직후 홈 화면에서 Loopback을 선택해 주자
테스트에서는 서버를 별도로 띄우지 않고 localhost:8080으로 띄우기 때문에 Loopback을 선택한다
다음으로 상단에 있는 빈칸에 http를 입력해 필터링을 걸어 http 통신만 볼 수 있도록 한다
맨 처음 설명하며 보여준 형태로 HTTP 요청을 쏴보면 패킷 출발 시점부터 파일명이 깨진 것을 볼 수 있다
문제를 만난 당시에는 서버에서 처리를 잘못했겠거니 싶었는데 헛다리 짚었다
왜 패킷이 나가는 시점부터 파일 이름이 깨져있을까? HTTP 스펙에 답이 있다
RFC2183#2.3 하단에 보면 아래 구절이 있다, RFC 표준에서 filename은 US-ASCII여야만 한다
한글은 US-ASCII 범위를 벗어나고 '.txt'는 US-ASCII로 표현이 가능하므로 ????.txt 가 완성된다
US-ASCII만 된다니.. 그럼 파일 이름은 무조건 영어야만 할까? 이 역시 HTTP 스펙에 답이 있다
RFC2231#4 상단에 보면 다음과 같은 설명이 있다
기본 형태인 US-ASCII value에 non-ASCII 문자를 쓰고 싶다면 charset, language를 지정해 줄 수 있다
변경된 형태는 parameter*={charset}'{language}'{encoded-value}이고 charset, language는 optional이다
*는 인코딩 되어 있단 의미로 사용되고 '는 구분자로 사용한다
인코딩하는 방식은 비공식적으로 RFC2231 encoding이라 부르고 구현체는 검색으로 흔히 나오진 않는 것 같다
인코딩 원리가 너무 궁금하다면 org.springframework.http.ContentDisposition과 RFC2047을 참고해 보자
서버 개발자가 직접 사용할 일은 없다고 봐도 된다
API 완성 후 한글 파일명의 파일로도 테스트했었는데 문제가 없었던 건 Postman을 클라이언트로 썼기 때문이다
Postman으로 테스트하여 결과를 보자, 헤더는 기본 설정 외에 Content-Type: multipart/form-data만 추가해 주면 된다
아래 사진과 같이 맞춘 후 요청을 보내보자
Postman에서 요청을 보내면 HTTP 스펙에 맞게 인코딩해준다
filename과 filename*이 모두 있다는 것과 filename의 값이 깨지지 않았다는 것이 눈에 띈다
filename과 filename*이 모두 존재하면 어떻게 될까?
RFC6266#4.3 하단을 보자, filename*이 우선시된다
결론으로 직접 파일 업로드 요청을 만들어야 하고 파일 이름에 다국어도 지원해야 한다면
HTTP 스펙을 지켜 filename*을 사용하고 인코딩 방법은 언어별 RFC2231 구현체를 찾아보자
POST http://localhost:8080/test/file-upload
Accept: application/json
Content-Type: multipart/form-data; boundary=---------------------------7d13a23b368
-----------------------------7d13a23b368
Content-Disposition: form-data; name="file"; filename="헬로월드.txt" filename*=UTF-8''%E1%84%92%E1%85%A6%E1%86%AF%E1%84%85%E1%85%A9%E1%84%8B%E1%85%AF%E1%86%AF%E1%84%83%E1%85%B3.txt
1234
-----------------------------7d13a23b368--
흔하게 발생하는 문제가 아니어서 그런지 해당 이슈를 상세하게 파악하는데 시간이 좀 걸렸다
시니어도 포함해서 꽤나 여럿이 같이 본 이슈인데 해결책은 Base64가 됐다
HTTP 스펙은 너무나 방대하다, 웹을 개발하는 많은 이들도 HTTP를 잘 모를 수밖에 없다
나도 예외는 아니고 이런 반가운 이슈를 만날 때마다 꾸준히 연구하는 수밖에 없다
덕분에 multipart/form-data에 대한 여러 RFC 문서들을 찾아보게 되었고 이해도 높아진 것 같다
문제 해결할 때에 여유가 조금만 더 있었더라면 표준에 맞는 방식으로 수정했을 텐데 하는 아쉬움이 있다
Base64로 땜빵 칠하고 더 나은 해결책을 고민하다니
한 가지 해결 방식도 겨우 고민해 내던 꼬꼬마 시절보다 문제 해결에 여유가 생기긴 했다
최근 들어 드는 생각은 문제를 어떻게 해결할 것인가에 관한 것이다
문제를 빠르게 해결하는 방법을 (Base64 인코딩) 써야 할 때도 있지만 그럼에도 표준을 따라가는 게 좋다
README, 주석을 쓰지 않는 클린 코더들의 코드를 유지보수 하려면
레드마인, 지라 등의 히스토리를 뒤져보고 히스토리가 없다면 무당의 심정으로 때려 맞추는 수밖에 없다
내가 작성한 코드도 이어받을 개발자가 '이걸 짠 놈은 무슨 생각으로 짠 거지' 싶은 코드가 될 수 있다
그때 당시에는 많은 고민을 하고 다양한 해결책 중에 유지보수할 이가 가장 이해하기 쉽도록 짰을 것이다
이 코드를 유지보수할 개발자에게 고통을 덜어주려면 연휴 후 출근에 일감 정리를 해놔야겠다
히스토리가 유실된다면 아쉬운 일이겠지만 이젠 이 글이 있지 않은가?
검색해서 못 찾으면 내가 지나온 RFC 탐방을 할 테고 운이 좋다면 이 글을 발견할 수도
'Spring > Spring MVC' 카테고리의 다른 글
CommonsMultipartResolver vs StandardServletMultipartResolver (0) | 2023.04.09 |
---|---|
@RequestBody, @ModelAttribute 매핑 방식의 이해 - 2 (0) | 2022.12.25 |
@RequestBody, @ModelAttribute 매핑 방식의 이해 - 1 (0) | 2022.11.13 |
[Swagger2] Failed to start bean 'documentationPluginsBootstrapper'; (0) | 2022.03.27 |