프로그램적인 파일 다운로드

웹에서 파일을 다운로드하는 방식은 두 가지로 나눌 수 있다. 하나는 Apache 등의 웹서버 프로그램을 통해 디스크 상에 있는 그대로의 파일을 다운로드하는 경우와 또 하나는 Java, .NET 등의 프로그램에서, 예를 들어 DB 조회 결과를 CSV, XLS 등으로 출력하거나 임의의 파일 내용을 출력하는 방법으로 프로그램적으로 파일을 다운로드하는 경우다.

두 가지 모두 HTTP 헤더 값에 따라 브라우저가 그 컨텐트를 다운로드할지 브라우저에서 보여줄지 결정된다. Apache는 실제 파일만을 취급하므로 파일 확장명 등에 따라 헤더가 결정돼 있지만 프로그램적으로는 개발자가 직접 설정해야 한다. 다음과 같은 내용으로 헤더를 설정하면 컨텐트가 다운로드된다.

Content-Disposition: attachment; filename="파일명"
Content-Transfer-Coding: binary

기본적으로 위 두 가지 헤더만 출력하면 되는 간단한 문제지만 위에서 파일명은 문자셋에 따라 상황이 복잡해진다.

1. 파일명이 8859-1 문자가 아니면 인코딩해야 한다

파일명이 영어 알파벳 등 기본 라틴 문자가 아닌 한글 완성형이나 utf-8 문자로 된 경우 파일명을 인코딩 또는 이스케이프해야 한다. Chrome, FireFox, Opera와 같이 RFC 2231 규격을 이해하는 브라우저에서는 아래 예와 같이 문자셋을 지정해 인코딩한다. 아래에서 “URL인코딩된파일명”은 %bc%c3%9f%e2%82%ac.txt와 같은 형식으로 문자를 인코딩한 것이다. 인코딩 방법은 아래 2번 설명을 참고한다.

Content-Disposition: attachment; filename*=utf-8''URL인코딩된파일명

그 외의 브라우저는 기본적으로 파일명을 URL 인코딩만하면 어느 정도 해결되기도 하지만 문자셋, 애플리케이션 서버에 영향을 많이 받고 특히 Safari는 2009년 4월 현재까지 한글 파일명을 제대로 처리하지 못하는 문제가 있다.

2. 파일명 인코딩 자체가 단순하지 않다

파일명을 인코딩하기 위해서는 Java의 경우 URLEncoder.encode, .NET에서는 HttpServerUtility.UrlEncode 메서드를, javascript에서는 encodeURIComponent를 일반적으로 사용하는데 이게 함정이 있다. 앞 둘의 클래스명을 보면 URL을 인코딩한다고 하지만 실제로는 URL 인코딩 표준이 아니라 HTTP 폼 인코딩 표준에 따른다. 그래서 몇 가지 차이점이 있는데 가장 큰 것이 빈칸을 %20이 아니라 +로 인코딩하는 것이다. 이 때문에 브라우저가 URL을 받았을 때 +는 빈 칸(space)으로 제대로 바뀌지 않는 문제점이 있다. %20만을 빈 칸으로 바꾸기 때문이다.

따라서 인코딩한 후 +는 %20으로 바꿔주는 것이 좋지만 이 경우는 파일명에 원래 +가 있는 경우 인코딩, 디코딩 과정에 또 문제가 발생하므로 Java의 경우 URLEncoder와 같은 클래스가 아니라 URI 같은 클래스를 사용하는 것이 좋을 수도 있다. URI는 원래 파일명 인코딩만을 위한 것이 아니라 호스트명 등도 포함해서 인코딩이 가능한데 문자셋을 지정할 수 없고 UTF-8만 되는 등의 한계가 있어 역시 완벽하진 않다. 따라서 파일명 인코딩을 위해서는 상황에 맞춰 적절한 클래스를 사용하거나 파일명에 빈칸, +가 있는 경우를 제한하는 방법 등을 생각해볼 필요가 있다.

3. 그 밖에 고려할 사항

위에서 두 가지 헤더만 설명했지만 Apache 같은 웹서버는 Content-Type 헤더도 설정하며 그에 따라 브라우저가 파일을 어떻게 보여주는지 바뀌기도 한다. 다시 말해 MIME 유형을 설정하는 것이 필요할 수도 있는데 Apache가 아니라 프로그램적으로는 모든 MIME을 처리하기는 부담이므로 필요한 형식 몇 가지만 미리 알고 있는 것이 좋다. (참고로 각 예시마다 필요한 경우 문자셋도 넣었다.)

  • HTML 형식 – “Content-Type: text/html; charset=utf-8”
  • Javascript 형식 – “Content-Type: application/x-javascript; charset=utf-8”
  • 스타일시트 – “Content-Type: text/css; charset=utf-8”
  • 일반 텍스트 형식 – “Content-Type: text/plain; charset=utf-8”
  • XML 텍스트 형식 – “Content-Type: text/xml; charset=utf-8”
  • 엑셀 형식 – “Content-Type: application/vnd.ms-excel”
  • PNG 형식 – “Content-Type: image/x-png”
  • JPG 형식 – “Content-Type: image/jpeg”
  • GIF 형식 – “Content-Type: image/gif”
  • 임의 파일 다운로드 형식 – “Content-Type: application/download”

프로그램적으로 생성하는 다운로드는 위의 것들이 가장 일반적이라 하겠다. 다른 형식은 MIME 유형에 대해 검색 등을 활용해보기 바란다.

4. 플래시 파일의 다운로드 또는 인라인 보기

플래시 파일(.swf)을 프로그램적으로 다운로드할 경우 파일을 직접 다운로드하는 것이 아니라 웹페이지에서 포함된 형태로 간접적으로 다운로드하게 하려면 얼마전에 보안상 바뀐 기준으로 인해(http://www.adobe.com/devnet/flashplayer/articles/fplayer10_security_changes_02.html) Content-Disposition을 attachment가 아니라 inline으로 해야 한다.

Content-Disposition: inline; filename="파일명"

5. 예시

이상과 같은 내용을 종합하면 다음과 같은 예시를 만들 수 있다. 메서드, JSP 태그 등에 따라 똑같은 내용도 다양하게 표현할 수 있으므로 실제 구현할 때는 좀 다른 결과가 나오는 것이 맞을 것이다.

Java:

String encoding = request.getCharacterEncoding();
String filename = Util.replace(URLEncoder.encode(filename, encoding), "+", "%20");
String disposition = filename.endsWith(".swf") ? "inline" : "attachment";
 
String agent = request.getHeader("User-Agent");
if (agent != null && agent.indexOf("Explorer") < 0)
    filename = "*=utf-8''" + filename;
else
    filename = "=\"" + filename + "\"";
 
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition", disposition + "; filename" + filename);
response.setHeader("Content-Transfer-Coding", "binary");

JSP:

<%@ page contentType="application/vnd.ms-excel"
%><% // 공백이 출력되지 않게 jsp 지시문 사이를 붙임
 
String encoding = request.getCharacterEncoding();
String filename = Util.replace(URLEncoder.encode(filename, encoding), "+", "%20");
String disposition = filename.endsWith(".swf") ? "inline" : "attachment";
 
String agent = request.getHeader("User-Agent");
if (agent != null && agent.indexOf("Explorer") < 0)
    filename = "*=utf-8''" + filename;
else
    filename = "=\"" + filename + "\"";
 
response.setHeader("Content-Disposition", disposition + "; filename" + filename);
response.setHeader("Content-Transfer-Coding", "binary");
%>

3 thoughts on “프로그램적인 파일 다운로드

  1. 좋은 팁 감사합니다~~ 덕분에 삽질의 늪에서 벗어날 수 있었습니다 ㅎㅎ

    저는 공백으로 split를 한 다음에 조각별로 인코딩 하여 이어 붙였습니다.
    이렇게 하면 +기호가 경로에 들어 있어도 문제가 되지 않더군요

    StringBuilder builder = new StringBuilder();
    name = "공백과 +기호가 같이 있으면.xlsx";
    for (String s : name.split(" ")) {
        if (builder.length() > 0) {
            builder.append("%20");
        }
        builder.append(URLEncoder.encode(s, "utf-8"));
    }
    
    1. 의견 정말 감사합니다. 그런데 제 글에 약간 부정확한 의견이 있었는데 + 기호가 있더라도 다음과 같이 처리하면 비교적 간단히 해결됩니다.

      URLEncoder.encode(url, "utf-8").replace("+", "%20")

    2. 아 단순한 문제를 엄청 복잡하게 해결했었네요 ㅎㅎ.
      덕분에 지저분한 소스를 한 줄로 다듬을 수 있었습니다. 좋은 팁 감사합니다~~

의견 있으시면 냉큼 작성해주세요~