Jay's Blog

[Boost_Camp] Spring 파일 업로드, 다운로드
2주차

파일 업로드와 파일 다운로드

1 pom.xml 파일에 파일 업로드와 관련된 라이브러리를 추가한다.

        <!-- file upload -->
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.2.1</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>1.4</version>
        </dependency>

2 ServletContextConfig 에 MultipartResolver Bean을 설정한다.

원래 HTTP 프로토콜은 파일 업로드를 지원하지 않았다. 추후에 파일업로드를 위해서 Body내용에 Multipart라는 기능을 이용하여 파일을 업로드 할 수 있도록 하였다. text파일을 여러개의 part로 나눠서 각 파트별로 파일내용을 인코딩하여 포함시키는 방식인데, 이 방식은 email에서 먼저 사용된 방식이었다.

참고 자료 : http://lng1982.tistory.com/209

스프링은 Multipart 지원 기능을 제공하고 있기 때문에, 이 기능을 이용하면 추가적인 처리없이 Multipart 형식으로 전송된 파라미터와 파일 정보를 쉽게 구할 수 있다.

    @Bean
    public MultipartResolver multipartResolver() {
        org.springframework.web.multipart.commons.CommonsMultipartResolver multipartResolver = new org.springframework.web.multipart.commons.CommonsMultipartResolver();
        multipartResolver.setMaxUploadSize(10485760); // 1024 * 1024 * 10
        return multipartResolver;
    }

3 WebInitializer 에 CharacterEncodingFilter를 설정하여 post로 값을 넘겨도 한글이 깨지지 않도록 하기

form의 method를 post방식으로 해야만 파일을 업로드 할 수 있다. request의 body에 파일정보가 전달됙 때문이다. 파일 정보와 함께 여러가지 파라미터를 함께 전달할 수 있는데, 이때 한글이 깨지는 경우가 있다. 한글이 깨지지 않도록 하기 위해서 보통 tomcat의 connector 설정에 encoding설정을 하고, 필터를 이용한다.

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {

        // Encoding Filter 설정, post로 값을 넘길때 깨지지 않도록
        EnumSet<DispatcherType> dispatcherTypes = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD);

        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        characterEncodingFilter.setForceEncoding(true);

        FilterRegistration.Dynamic characterEncoding = servletContext.addFilter("characterEncoding", characterEncodingFilter);
        characterEncoding.addMappingForUrlPatterns(dispatcherTypes, true, "/*");

        // dispatcherServlet 설정
        WebApplicationContext context = getContext();
        servletContext.addListener(new ContextLoaderListener(context));
        ServletRegistration.Dynamic dispatcher = servletContext.addServlet("DispatcherServlet", new DispatcherServlet(context));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping(MAPPING_URL);
    }

4 파일 업로드 form 작성하기. /WEB-INF/views/files.jsp

form을 보면 enctype부분이 multipart/form-data 로 정의된 것을 알 수 있다. 반드시 파일을 업로드할 때는 해당 속성이 지정되어 있어야 한다. method는 반드시 post여야 한다.

input type=”file”인 경우 name이 모두 file인 것을 알 수 있다. 이 경우 Controller에게 name이라는 이름으로 파라미터가 배열로 전달된다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>file등록 폼</title>
</head>
<body>
<form method="post" action="/files" enctype="multipart/form-data">
    title : <input type="text" name="title"><br>
    <input type="file" name="file"><br>
    <input type="file" name="file"><br>
    <input type="submit" value="등록">
</form>
</body>
</html>

5 upload & 다운로드

위에 대한 설명은 코드로 대신한다.

package carami.todo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;


@Controller
@RequestMapping("/files")
public class FilesController {
    private String baseDir = "c:" + File.separator + "temp" + File.separator; // c:\temp 디렉토리를 미리 만들어둔다.

    @GetMapping
    public String fileform(){
        return "files";
    }

    @PostMapping
    public String create(
            @RequestParam("title") String title,
            @RequestParam("file") MultipartFile[] files){

        if(files != null && files.length > 0){

            // windows 사용자라면 "c:\temp\년도\월\일" 형태의 문자열을 구한다.
            String formattedDate = baseDir + new SimpleDateFormat("yyyy" + File.separator + "MM" + File.separator + "dd").format(new Date());
            File f = new File(formattedDate);
            if(!f.exists()){ // 파일이 존재하지 않는다면
                f.mkdirs(); // 해당 디렉토리를 만든다. 하위폴더까지 한꺼번에 만든다.
            }

            for(MultipartFile file : files) {
                String contentType = file.getContentType();
                String name = file.getName();
                String originalFilename = file.getOriginalFilename();
                long size = file.getSize();

                String uuid = UUID.randomUUID().toString(); // 중복될 일이 거의 없다.
                String saveFileName = formattedDate + File.separator + uuid; // 실제 저장되는 파일의 절대 경로

                // 아래에서 출력되는 결과는 모두 database에 저장되야 한다.
                // pk 값은 자동으로 생성되도록 한다.
                System.out.println("title :" + title);
                System.out.println("contentType :" + contentType);
                System.out.println("name :" + name);
                System.out.println("originalFilename : " + originalFilename);
                System.out.println("size : " + size);
                System.out.println("saveFileName : " + saveFileName);

                // 실제 파일을 저장함.
                // try-with-resource 구문. close()를 할 필요가 없다. java 7 이상에서 가능
                try(
                        InputStream in = file.getInputStream();
                        FileOutputStream fos = new FileOutputStream(saveFileName)){
                    int readCount = 0;
                    byte[] buffer = new byte[512];
                    while((readCount = in.read(buffer)) != -1){
                        fos.write(buffer,0,readCount);
                    }
                }catch(Exception ex){
                    ex.printStackTrace();
                }
            } // for
        } // if

        return "redirect:/files";
    }

    // files/dbPk값 을 받아들여서 다운로드 하도록 한다.
    // 여기에서는 db에서 읽어들였다는 것을 가정하고 프로그래밍한다.
    @GetMapping(path="/{id}")
    public void downloadReservationUserCommentImage(
            @PathVariable(name="id") long id,
            HttpServletResponse response
    ){
        // id를 이용하여 파일의 정보를 읽어온다.
        // 이 부분은 원래 db에서 읽어오는 것인데 db부분은 생략했다.

        String originalFilename = "원본파일명";
        String contentType = "image/jpeg";
        int fileSize = 271621;
        // 해당 파일이 이미 존재해야한다.
        String saveFileName = "c:/temp/2017/07/12/61405ccf-5147-493a-9b9a-ef0375e40dfd";

        response.setHeader("Content-Disposition", "attachment; filename=\"" + originalFilename + "\";");
        response.setHeader("Content-Transfer-Encoding", "binary");
        response.setHeader("Content-Type", contentType);
        response.setHeader("Content-Length", ""+ fileSize);
        response.setHeader("Pragma", "no-cache;");
        response.setHeader("Expires", "-1;");

        java.io.File readFile = new java.io.File(saveFileName);
        if(!readFile.exists()){ // 파일이 존재하지 않다면
            throw new RuntimeException("file not found");
        }

        FileInputStream fis = null;
        try{
            fis = new FileInputStream(readFile);
            FileCopyUtils.copy(fis, response.getOutputStream()); // 파일을 저장할때도 사용할 수 있다.
            response.getOutputStream().flush();
        }catch(Exception ex){
            throw new RuntimeException(ex);
        }finally {
            try {
                fis.close();
            }catch(Exception ex){
                // 아무것도 하지 않음 (필요한 로그를 남기는 정도의 작업만 함.)
            }
        }

    }

}
*****
Written by Jay on 13 July 2017