프로그래밍/JAVA

[JAVA] 파일 다운로드 시 한글 파일명 깨짐 문제 해결 — Content-Disposition 인코딩

이슬먹는 개발자 2026. 6. 30. 16:12
728x90
반응형

분명히 "보고서.pdf"로 올렸는데, 다운로드하면 왜 깨질까?

지난 글에서 이미지 업로드의 EXIF 회전 문제를 다뤘는데, 파일을 다룰 때 EXIF 못지않게 사람을 괴롭히는 게 또 하나 있다. 바로 파일 다운로드 시 한글 파일명이 깨지는 문제다.

분명히 서버에 결산보고서_2026.xlsx로 저장했는데, 사용자가 다운로드하면 ê²°ì‚°ë³´ê³ ì„œ.xlsx 같은 외계어가 나온다. 이 글을 클릭한 사람이라면 아마 이 외계어를 직접 마주하고 들어왔을 거다. 오늘은 이 문제가 왜 생기는지, 그리고 어떻게 깔끔하게 해결하는지 처음부터 끝까지 정리해보겠다.


먼저 현상을 정확히 파악해보자

상황은 보통 이렇다.

  1. 사용자가 매출현황.xlsx 파일을 업로드한다.
  2. 서버는 잘 저장한다. (여기까지는 멀쩡하다.)
  3. 다른 사용자가 그 파일을 다운로드한다.
  4. 다운로드된 파일 이름이 매출íˆí™©.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.xlsx

UTF-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만 안다"는 사실 하나로 귀결된다. 그 사실만 이해하면 해결책은 자연스럽게 따라온다.

  • 빠르게 막으려면 → filename URL 인코딩
  • 제대로 하려면 → filename* (RFC 5987)
  • 가장 편하게 하려면 → Spring ContentDisposition

다음에 또 파일 다룰 때 외계어가 나오면, 당황하지 말고 헤더부터 확인해 보자.

728x90
반응형

'프로그래밍 > 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