IBM®
메인 컨텐츠로 가기
    Korea [국가변경]    이용약관
 
 
   
        제품    서비스 & 솔루션    고객지원 & 다운로드    회원 서비스    
한국 developerWorks   >  dW Column  > developerworks

S 서블릿 3.0에서 파일 업로드



이창신이창신 iasandcb@gmail.com, http://iasandcb.pe.kr

티맥스소프트 JEUS팀, 엔씨소프트 오픈마루 스튜디오를 거쳐 현재 FLOO의 CTO로 재직중이다.

2009년 10월 6일


웹 애플리케이션에서 파일 업로드는 피할 수 없는 기능이다. 필자도 오픈 소스 개발에 참여한 계기가 바로 파일 업로드 때문이었기에 인연이 별나다. 필자의 첫 번역서였던 자바 서블릿 프로그래밍 개정판에는 파일 업로드 라이브러리(com.oreilly.servlet 패키지여서 COS라는 이름이 붙었는데 http://servlets.com/cos/에서 지금도 배포되고 있다)가 소개되어 있었는데, 이 라이브러리가 이름이 한글로 된 파일을 처리하지 못하는 문제가 있었다. 그래서 필자와 원저자(Jason Hunter)가 이메일로 연락을 하면서 그 문제를 해결했다. 책이 나오고 나서 안 사실이지만, 그 라이브러리가 꽤 쓰이고 있었는데 한글 이름 파일 문제도 애로가 많았던 모양이다. 한글 이름 파일 문제까지 해결된 COS에 대해 문의를 몇 차례 받으면서 이렇게 자주 쓰이는 기능이 왜 서블릿 표준안에 들어가 있지 않을까 의아했다.

물론 이제는 아파치의 FileUpload(http://commons.apache.org/fileupload/)와 같이 사실상의 표준(de facto standard) 라이브러리가 널리 쓰이지만, 매우 기본적인 기능에 대한 코드를 표준 수준에서 이식성 있게 작성하지 못한다는 점은 아쉽다. 그런데 그동안 좌절(특히 서블릿 2.5 스펙에서는 매우 의욕적으로 시도했지만)을 몇 번 겪은 파일 업로드 기능 표준화가 드디어 서블릿 3.0 스펙에 들어가게 되었다. 현재 서블릿 3.0의 파일 업로드 기능은 글래스피시(GlassFIsh) v3에서 바로 사용해볼 수 있다. 그래서 이 글을 통해 간단히 소개해보려 한다.

고대하던 파일 업로드 기능 맛보기

먼저, 편한 개발을 위해 넷빈즈(NetBeans) 6.8M1을 설치하자. http://bits.netbeans.org/netbeans/6.8/m1/에서 받을 수 있으며, Java나 All 번들을 받으면 된다. 참고로 넷빈즈 6.8에는 글래스피시 v3가 내장되어 있어 따로 설치할 필요가 없다. 주의할 것은 글래스피시 v3는 자바 SE 6 이상을 요구하므로 자신의 JDK 버전을 확인해두기 바란다.

그러면 넷빈즈를 띄운 다음, File > New Project를 선택하면 프로젝트 생성 마법사가 나온다. 1단계로 카테고리(Categories)에서 Java Web을, Projects에서는 Web Application을 선택한 다음 Next 버튼을 누르면 프로젝트 이름(Project Name)을 정하는데 Uploader라고 하고 Next 버튼을 누른 다음 서버 관련 설정은 주어진 대로 두고 Finish 버튼을 눌러 프로젝트 생성을 완료한다.

프로젝트가 마련되었으니 업로드 기능을 구현해보자. Uploader 프로젝트의 Web Pages 밑으로 index.jsp 파일을 다음과 같이 작성한다.

Listing 1. index.jsp

<%@page contentType="text/html" pageEncoding="UTF-8"%> 
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
   "http://www.w3.org/TR/html4/loose.dtd"> 

<html> 
    <head> 
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 
        <title>Upload</title> 
    </head> 
    <body> 
        <form action="post" method="post" enctype="multipart/form-data"> 
            <input type="text" name="name"> 
            <input type="file" name="file"/> 
            <input type="submit"/> 
        </form> 
    </body> 
</html> 


Uploader 애플리케이션을 배포(deploy)한 다음, http://localhost:8080/Uploader/index.jsp를 브라우저로 열어보면 아주 간단한 텍스트 필드 입력 창과 파일 선택 버튼, 그리고 전송 버튼이 나온다.

이제 이 페이지에서 올리는 파일을 받는 서블릿을 작성해보자. Source Packages 밑으로 ias 패키지를 만들고 그 밑으로 UploadServlet.java 파일을 아래와 같이 작성한다.

Listing 2. UploadServlet.java

package ias; 

import java.io.BufferedReader; 
import java.io.IOException; 
import java.io.InputStream; 
import java.io.InputStreamReader; 
import javax.servlet.ServletException; 
import javax.servlet.annotation.WebServlet; 
import javax.servlet.annotation.MultipartConfig; 
import javax.servlet.http.HttpServlet; 
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse; 
import javax.servlet.http.Part; 

@MultipartConfig(location = "/Users/ias", maxFileSize = 1024 * 1024 * 10, fileSizeThreshold = 1024 * 1024, maxRequestSize = 1024 * 1024 * 20) 

@WebServlet(name = "UploadServlet", urlPatterns = {"/post"}) 
public class UploadServlet extends HttpServlet { 

    @Override 
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException { 
        System.out.println(request.getParameter("name")); // This doesn't work 
        String fileName = "dummy"; 
        Part filePart = null; 
        for (Part part : request.getParts()) { 
            System.out.println(part); 
            for (String headerName : part.getHeaderNames()) { 
                System.out.println(headerName); 
                System.out.println("-"); 
                System.out.println(part.getHeader(headerName)); 
                // To find out file name, parse header value of content-disposition 
                // e.g. form-data; name="file"; filename="" 
            } 
            // Get a normal parameter 
            if (part.getName().equals("name")) { 
                String paramValue = getStringFromStream(part.getInputStream()); 
                System.out.println(paramValue); 
                fileName = paramValue; 
            } 
            if (part.getName().equals("file")) { 
                filePart = part; // Absolute path doesn't work. 
            } 
        } 
        filePart.write(fileName); 
        response.sendRedirect("index.jsp"); 
    } 

    public String getStringFromStream(InputStream stream) 
            throws IOException { 
        BufferedReader br = new BufferedReader(new InputStreamReader(stream, "UTF-8")); 
        StringBuilder sb = new StringBuilder(); 
        String line = null; 

        try { 
            while ((line = br.readLine()) != null) { 
                sb.append(line + "\n"); 
            } 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } finally { 
            try { 
                stream.close(); 
            } catch (IOException e) { 
                e.printStackTrace(); 
            } 
        } 
        return sb.toString(); 
    } 
} 


지난 컬럼에서 소개했듯이, 서블릿 3.0에서는 서블릿 작성에 수반되는 web.xml을 작성할 필요가 없다. 정확히는, web.xml의 역할을 @WebServlet 어노테이션이 한다. Listing 2에서는 UploadServlet을 부르기 위해 /post(배포된 후라면 http://localhost:8080/Upload/post)라는 URL을 쓰도록 설정했다.

업로드 관련 설정도 @MultipartConfig로 한다. 먼저 location은 업로드된 파일이 기본적으로 저장될 위치인데, 자세한 사용법은 코드와 함께 설명하겠다. maxFileSize는 업로드된 파일 한 개의 크기 제한으로 위에서는 10메가바이트(1024 * 1024 * 10)로 했다. fileSizeThreshold는 업로드된 파일이 임시로 서버에 파일로 저장되지 않고 메모리에서 바로 스트림으로 전달되는 크기의 한계를 가리키는데, 예를 들어 위와 같이 1024 * 1024, 즉 1메가바이트로 했을 경우 파일이 그 이상인 경우만 임시 파일로 저장되므로 1메가바이트 이하의 파일은 빠르게 전해지는 대신 문제가 생겼을 때 복원하기 어려울 수 있다. maxRequestSize는 파일 한 개의 용량 제한이 아닌 전체 multipart/form-data 인코딩 요청의 크기 제한이다. 이는 하나의 요청에 파일 여러 개가 업로드될 경우 총합에 대한 제한이라고 볼 수 있다.

doPost 메서드 안에서 흥미롭게 볼 것이 몇 가지 있다. 우선 파일 업로드 시에 사용하는 multipart/form-data 인코딩으로 요청이 오는 경우 일반적인 HttpServletRequest의 getParameter 메서드로는 파일이 아닌 다른 폼 필드 데이터에 접근할 수 없다. 앞서 index.jsp에서는 name이라는 이름의 텍스트 필드가 그 경우인데, 서블릿을 실행해보면 request.getParameter("name") 값이 null로 나온다.

그 이유는, multipart/form-data의 경우 일반 매개변수도 모두 한 파트(javax.servlet.http.Part)로 끊어져 넘어오기 때문인데, 이를 처리하기 위해 HttpServletRequest의 getParts를 얻어 순환하게 된다. 이때 주의할 것은, 파일 크기 제한과 같은 제한을 벗어나는 경우 ServletException이 발생하므로 적절하게 처리해줘야 한다는 의무 사항이다.

파트를 얻어온 다음, 파트에 대한 정보를 얻어내는 일이 중요하다. 파트를 단순히 System.out으로 출력해보면 name과 같이 일반 필드의 경우


File name=null, StoreLocation=/Users/ias/upload__4441d94f_124256018e1__7fec_00000004.tmp, size=8bytes, isFormField=true, FieldName=name

과 같이 파일 이름이 null인 대신 isFormField가 true이고, file과 같이 파일 필드의 경우


File name=iPhone_Games_Projects-4448.pdf, StoreLocation=/Users/ias/upload__4441d94f_124256018e1__7fec_00000005.tmp, size=878389bytes, isFormField=false, FieldName=file 

와 같이 파일 이름이 넘어오고 isFormField는 false가 된다.

파일이 아닌 필드의 경우 매개변수 값을 얻어오는 것이 필요한데, 파트의 경우 바로 문자열로 주지 않아 스트림을 문자열로 변환해야 한다(getStringFromStream이 그 일을 한다).

파일인 경우 저장을 해야 할 텐데, Part의 write 메서드를 사용하면 편하다. write의 인수로 위 예제와 같이 name 필드의 값으로 간단히 파일 이름을 주면 되는데, @MultipartConfig의 location에서 설정한 경로 밑으로 저장된다. 물론 이렇게 저장하지 않고 자체적인 저장 로직을 작성하려면 Part의 getInputStream 메서드로 스트림을 직접 얻어 처리하면 된다.

파일의 메타데이터는 헤더로 넘어오는데, content-type의 경우 Part의 getContentType 메서드로 얻을 수 있지만, 파일 이름은 content-disposition 헤더 값을 파싱(parsing)해야 한다. 예를 들어 다음과 같은 문자열이 content-disposition 헤더 값으로 넘어오는데, filename=""에 해당하는 문자열을 뽑아 내야 한다.


form-data; name="file"; filename="iPhone_Games_Projects-4448.pdf" 


마치며

필자가 COS를 접한 지 물경 10년째가 다 되어가는 2009년, 파일 업로드가 서블릿 3.0 스펙으로 표준화됐다는 사실에 격세지감을 몸으로 느끼는 경험을 이 글을 통해 하게 되었다. 다음 컬럼에는 완성된 자바 EE 6과 글래스피시 v3, 그리고 넷빈즈 6.8을 소개할 수 있기를 고대한다.



이 문서 북마킹 하기

mar.gar.in mar.gar.in naver naver eolin eolin del.icio.us del.icio.us



위로


[지난 developerWorks Column 보기]

사이트 여행

dW 커뮤니티
포럼 | 블로그 | Spaces
dW Student Community

로컬 컨텐츠

행사 및 세미나

기획 기사

개발자 입문

튜토리얼 및 교육

TOP 10 인기자료

SW 다운로드

RSS 피드

뉴스레터
 
  
자바스크립트가 작동이 중지되었습니다. 이 기능을 수행하시려면 브라우저에서 자바스크립스트를 작동시켜 주시거나 이곳을 클릭해주세요.

Special offers
Screencast
IBM SOA Sandbox 시험판
dW Student Community
로보코드
코드 트레이닝


    IBM 소개 개인정보 보호정책 문의