문제

파일 업로드를 구현하고 업로드 한 파일을 다운받기 위해 기능을 구현하였다. 그런데 브라우져 별로 테스트를 해보니 문제가 있었다.

크롬(Chrome) : 파일 다운로드는 되지만 파일명의 특수문자 깨짐

익스플로러(Internet Explorer) : 크롬의 문제 + 한글 파일명을 가진 파일은 다운로드 자체가 안됨

파이어폭스(Firefox) : 파일에 공백이 있으면 그 공백을 기준으로 뒤쪽 이름은 다 짤려버림. 예를 들어 “테스트 파일.jpg”를 다운로드 해보면 “테스트”라는 이름으로 확장자도 안 붙은 파일이 다운로드 됨

원인 및 해결

크롬(Chrome)

구글링으로 찾아보니 브라우져별로 파일명 인코딩을 해줘야한다고 한다.

fileNameOrg = new String(fileNameOrg.getBytes("UTF-8"), "ISO-8859-1");

크롬은 위와 같이 파일명을 UTF-8 인코딩 형태의 바이트로 받아서 ISO-8859-1로 문자를 생성해주면 된다고 한다.

익스플로러(Internet Explorer)

다른 브라우져와 달리 익스플로러는 URL에 한글이 들어가면 인코딩 과정을 거치지 않고 한글 그대로를 보여준다고 한다. 그래서 URL에 한글이 들어가면 다운로드 자체가 안 되던 것이었다. 다운로드를 하는 컨트롤러를 호출할 때, 한글 파일명은 인코딩하여 보내주도록 하면 된다.

나는 현재 다운로드 컨트롤러를 호출할 때 "/download.do?filePath=&fileName&fileNameOrg=" 이런식으로 호출한다.

그래서 만약 "/download.do?filePath=board&fileName=1_1.jpg&fileNameOrg=테스트 파일.jpg" 이런식으로 호출하면 파일명에 한글이 들어가있기 때문에 오류가 나는 것이다.

그래서 파일명을 URLEncoder.encode(fileName, "UTF-8"); 과 같이 인코딩 처리를 한번 해주고 넣어주면 문제 없이 잘 된다.

파이어폭스(Firefox)

파이어폭스는 앞의 두개와는 아예 별개의 문제였다. 파일명에 공백이 있으면 그 공백 뒤로는 파일명이 다 짤려버리고 있어서 문제를 찾아보니 다운로드 소스 쪽에 문제가 있었다. 예전 파이어폭스는 괜찮은데 어느 버전부터인가 아래와 같이 처리를 해주지 않으면 파일명이 이상하게 받아진다고 한다.

response.setHeader("Content-Disposition", "attachment; filename=\"" + fileNameOrg + "\"");

filename을 ““로 감싸주어 파일명이 온전하게(?) 보존되도록 해준다. 그러면 멀쩡하게 받아진다.

다운로드 최종 소스

try{                                        
    request.setCharacterEncoding("UTF-8");
    fileName = new String(fileName.getBytes("UTF-8"), "UTF-8");
    File file = new File(filePath, fileName);
                
    if(file.isFile()){
        int bytes = (int)file.length();
        String header = request.getHeader("User-Agent");

        if (header.contains("MSIE") || header.contains("Trident")) {
            fileNameOrg = URLEncoder.encode(fileNameOrg,"UTF-8").replaceAll("\\+", "%20");
            response.setHeader("Content-Disposition", "attachment;filename=" + fileNameOrg + ";");
        } else {
            fileNameOrg = new String(fileNameOrg.getBytes("UTF-8"), "ISO-8859-1");
           response.setHeader("Content-Disposition", "attachment; filename=\"" + fileNameOrg + "\"");
        }
         
        response.setContentType( "application/download; UTF-8" );
        response.setContentLength(bytes);
        response.setHeader("Content-Type", "application/octet-stream");                
        response.setHeader("Content-Transfer-Encoding", "binary;");
        response.setHeader("Pragma", "no-cache;");
        response.setHeader("Expires", "-1;");
        
        BufferedInputStream fin = new BufferedInputStream(new FileInputStream(file));
        BufferedOutputStream outs = new BufferedOutputStream(response.getOutputStream());
        
        byte[] readByte = new byte[4096];
        try{
            while((bytes = fin.read(readByte)) > 0){
                outs.write(readByte, 0, bytes);
                outs.flush();
            }
        }
        catch(Exception ex){
            ...
        }
        finally{
            outs.close();
            fin.close();
        }
    }
}catch(Exception ex){
    ...
}

filePath는 파일이 있는 경로, fileName은 저장된 파일명(원본 파일명으로 저장하면 파일명이 겹칠 때, 이전 파일에 덮어씌워지는 문제와 관리가 어렵기 때문에 보통은 게시물 PK + Index를 조합하여 파일명을 변환한 뒤 저장한다.), fileNameOrg는 저장된 파일의 원래 파일명을 가리킨다.

@RequestParam을 통해 filePath 등의 파라미터 값을 받아서 try 문 안에서 재가공 해준다. 먼저 실제 저장된 파일명인 fileName을 통해 File 객체를 선언해준다. isFile()을 통해 실제로 파일이 존재한다면 그 파일을 다운로드 받는 과정을 거친다. 거기서 fileNameOrg가 쓰이게 된다. 사용자가 다운 받는 파일의 이름은 변환된 파일명이 아닌, 원래 파일명이 나와야하기 때문에 재가공하는 과정을 거쳐주는 것이다.

주로 크롬에서 개발을 진행하다보니 테스트도 크롬에서만 하게 된다. 그러다가 운영 서버에 반영하고 얼마 안 가 익스플로러나 파이어폭스 사용자의 문의가 들어오곤 한다. 그래서 이제 테스트할 때도 웬만하면 크롬, 익스, 파폭 세 개 다 해보려고 노력 중이다.-_- 여러 케이스를 최대한 고려해서 기능을 구현하려면 아직도 갈 길이 먼 것 같다.