분명히 "보고서.pdf"로 올렸는데, 다운로드하면 왜 깨질까?
지난 글에서 이미지 업로드의 EXIF 회전 문제를 다뤘는데, 파일을 다룰 때 EXIF 못지않게 사람을 괴롭히는 게 또 하나 있다. 바로 파일 다운로드 시 한글 파일명이 깨지는 문제다.
분명히 서버에 결산보고서_2026.xlsx로 저장했는데, 사용자가 다운로드하면 ê²°ì‚°ë³´ê³ ì„œ.xlsx 같은 외계어가 나온다. 이 글을 클릭한 사람이라면 아마 이 외계어를 직접 마주하고 들어왔을 거다. 오늘은 이 문제가 왜 생기는지, 그리고 어떻게 깔끔하게 해결하는지 처음부터 끝까지 정리해보겠다.
먼저 현상을 정확히 파악해보자
상황은 보통 이렇다.
- 사용자가
매출현황.xlsx파일을 업로드한다. - 서버는 잘 저장한다. (여기까지는 멀쩡하다.)
- 다른 사용자가 그 파일을 다운로드한다.
- 다운로드된 파일 이름이
매출íˆí™©.xlsx또는%EB%A7%A4%ECB6%9C처럼 깨져서 내려온다.
영문 파일명은 멀쩡한데 한글만 깨진다면, 거의 100% HTTP 헤더 인코딩 문제다. 내 코드가 잘못된 게 아니라, HTTP 헤더의 오래된 규칙 때문이다.
원인: HTTP 헤더는 원래 ASCII만 안다
파일을 다운로드시킬 때 보통 이런 헤더를 내려준다.
Content-Disposition: attachment; filename="매출현황.xlsx"문제는 여기에 있다. HTTP 헤더 값은 기본적으로 ASCII(영문/숫자/일부 기호)만 안전하게 표현할 수 있도록 설계되었다. 한글, 일본어, 이모지처럼 ASCII를 벗어나는 문자는 헤더에 그대로 넣으면 브라우저마다 제멋대로 해석한다.
그래서 한글 파일명을 그냥 넣으면
- 어떤 브라우저는 깨진 글자로 표시하고
- 어떤 브라우저는 아예 파일명을 무시하고
- 어떤 환경에서는 다운로드 자체가 실패하기도 한다.
해결책 1: filename을 URL 인코딩하기
가장 기본적인 해결책은 파일명을 URL 인코딩(percent-encoding) 해서 ASCII 범위 안으로 바꿔주는 거다.
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
String fileName = "매출현황.xlsx";
// UTF-8로 URL 인코딩
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8)
.replaceAll("\\+", "%20"); // 공백이 +로 바뀌는 것 방지
response.setHeader(
"Content-Disposition",
"attachment; filename=\"" + encodedFileName + "\""
);
여기서 replaceAll("\\+", "%20")을 꼭 기억하자. URLEncoder는 공백을 +로 바꾸는데, 파일명에서는 +가 그대로 살아남아 이름이 이상해진다. 그래서 %20으로 다시 바꿔줘야 한다.
이 방법은 대부분의 브라우저에서 잘 동작한다. 다만 구형 브라우저나 특정 환경에서는 여전히 어긋날 수 있어서, 더 견고한 방법이 필요하다.
해결책 2: RFC 5987 — filename* 사용하기 (권장)
위 문제를 표준 차원에서 해결하기 위해 나온 게 RFC 5987이다. 핵심은 filename 대신 별표가 붙은 filename*를 쓰고, 거기에 문자셋과 인코딩 정보를 같이 명시하는 거다.
형식은 이렇다.
Content-Disposition: attachment; filename*=UTF-8''%EB%A7%A4%EC%B6%9C%ED%98%84%ED%99%A9.xlsxUTF-8'' 부분이 "이 파일명은 UTF-8로 인코딩되어 있다"고 브라우저에게 알려주는 역할을 한다. 자바 코드로는 이렇게 작성한다.
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
String fileName = "매출현황.xlsx";
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
response.setHeader(
"Content-Disposition",
"attachment; filename*=UTF-8''" + encodedFileName
);
filename과 달리 값을 큰따옴표로 감싸지 않는다는 점에 주의하자. filename*=UTF-8''... 형태가 정확한 문법이다.
해결책 3: 구형 + 신형 브라우저 모두 대응하기
가장 안전한 방법은 두 가지를 동시에 내려주는 거다. 구형 브라우저는 filename을, RFC 5987을 지원하는 브라우저는 filename*를 읽는다. 표준상 둘 다 있을 경우 filename*가 우선된다.
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public void download(HttpServletResponse response, String fileName) {
String encoded = URLEncoder.encode(fileName, StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
String contentDisposition = "attachment; "
+ "filename=\"" + encoded + "\"; "
+ "filename*=UTF-8''" + encoded;
response.setHeader("Content-Disposition", contentDisposition);
// 이후 파일 스트림을 response 로 내려주면 끝
}
Spring을 쓴다면 더 간단하게
스프링을 사용한다면 ContentDisposition 빌더가 위 과정을 알아서 처리해 준다. 직접 인코딩 문자열을 조립할 필요가 없어 실수할 여지가 줄어든다.
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import java.nio.charset.StandardCharsets;
ContentDisposition contentDisposition = ContentDisposition.attachment()
.filename(fileName, StandardCharsets.UTF_8)
.build();
HttpHeaders headers = new HttpHeaders();
headers.setContentDisposition(contentDisposition);
filename(이름, charset)에 UTF-8을 넘기면 내부적으로 RFC 5987 형식(filename*)으로 만들어 준다. 실무에서는 이 방식이 가장 깔끔하다.
정리
| 방법 | 특징 | 추천도 |
|---|---|---|
| filename URL 인코딩 | 간단, 대부분 동작 | ⭐⭐ |
| filename* (RFC 5987) | 표준 방식, 안전 | ⭐⭐⭐ |
| 둘 다 내려주기 | 구형까지 호환 | ⭐⭐⭐ |
| Spring ContentDisposition | 가장 간편 | ⭐⭐⭐⭐ |
한글 파일명 깨짐은 결국 "HTTP 헤더는 원래 ASCII만 안다"는 사실 하나로 귀결된다. 그 사실만 이해하면 해결책은 자연스럽게 따라온다.
- 빠르게 막으려면 →
filenameURL 인코딩 - 제대로 하려면 →
filename*(RFC 5987) - 가장 편하게 하려면 → Spring
ContentDisposition
다음에 또 파일 다룰 때 외계어가 나오면, 당황하지 말고 헤더부터 확인해 보자.
'프로그래밍 > JAVA' 카테고리의 다른 글
| [JAVA] 이미지 업로드 회전 문제 해결 -- EXIF Orientation (0) | 2026.02.21 |
|---|---|
| [JPA] 연관관계(2) (0) | 2021.07.19 |
| [JPA] 연관관계 (1) (0) | 2021.07.18 |
| [JPA] EntityListeners 활용 (0) | 2021.07.17 |
| [JPA] QueryMethod 활용 (0) | 2021.07.17 |