[Spring Framework] 41. Spring 방식의 파일 업로드, 다운로드(Commons-io, FileUpload) - (XML, Java)
업로드에 관한 내용이다.
방식이 두 가지가 있다. 셋팅에 있어서 다소 차이가 있을 수 있다.
수 차례 실험을 해본 결과로는 Java 방식을 사용하고자 했을 때는 CGLib를 추가로 넣어줘야 한다.
[참고]
해당 코드는 Spring Framework 내에서만 동작하는 방법이다.
JSP/Servlet 업로드, 다운로드 관련해서는 JSP 카테고리에서 살펴보면 되겠다.
[개발 환경]
* IDE: Eclipse 2020-06
* Framework:
- Spring Framework 4.2.4 RELEASES
- commons-fileupload (2.8)
- commons-io (1.4)
- CGLIB (3.3.0) - Java 환경 설정 방식만 해당 [의외로 사용 빈도가 있는 jar 파일이다.]
(RootConfig.java 파일과 Beans 등 사용하면 필요할 때가 있음.)
- Java Version 1.8 (OpenJDK 15)
1. 프로젝트 구성도
공통적으로 작성한 파일은 한번만 소개하고 변경된 사항들만 분리 요약해서 작성하도록 하겠다.
"FileController.java" / 공통
"HomeController.java" / 공통
"insert.jsp" / 공통
|
|
그림 1. XML 파일 방식 |
그림 2. Java 방식 |
2. Java Compiler, Build Path, Project Desc (공통)
* Java Compiler - compiler compliance level 1.8
* Build Path
- Add Jar로 ojdbc.jar 파일 등록해주기
* Project Factes - Java : 1.8
3. pom.xml (XML, Java 방식)
(중략) <properties> <java-version>1.8</java-version> <org.springframework-version>4.2.4.RELEASE</org.springframework-version> <org.aspectj-version>1.6.10</org.aspectj-version> <org.slf4j-version>1.6.6</org.slf4j-version>
</properties>
(중략) |
공통 - pom.xml |
<!-- https://search.maven.org/artifact/commons-fileupload/commons-fileupload/1.4/jar -->
<dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.8.0</version> </dependency>
|
<!-- https://search.maven.org/artifact/commons-fileupload/commons-fileupload/1.4/jar -->
<dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.8.0</version> </dependency>
<!-- 자바 방식 - FileUpload에서는 CGLIB 넣어줘야 함 --> <!-- https://mvnrepository.com/artifact/cglib/cglib --> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency> |
XML 방식 - pom.xml |
Java 방식 - pom.xml |
하나 차이 밖에 없는데, cglib 라이브러리가 있느냐 없느냐 이런 차이인데, 그냥 다 써도 무방하다.
분리해서 소개한 이유는 최소의 라이브러리로 돌아가는지 태스트 해보려고 그랬다.
디버그 창에 종종 스프링 작업을 하면, 오류로 라이브러리가 필요하다고 올라오는데 그런 예기치 못한 작업들도 생각할 수 있으면 좋겠다.
4. insert.jsp (공통)
스프링을 공부하면서 연습 템플릿이 여의치 않아서 삽질을 다소 많이 하였다.
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ page session="false" %>
<html>
<head>
<meta charset="UTF-8">
<title>File Upload - Insert 페이지</title>
</head>
<body>
<h1>
Hello world!
</h1>
<h1>파일 업로드</h1>
<form action="fileupload" method="post" enctype="multipart/form-data">
<input type="file" name="uploadfile" placeholder="파일 선택" /><br/>
<input type="submit" value="업로드">
</form>
<h2>다중 파일 업로드</h2>
<form action="fileupload2" method="post" enctype="multipart/form-data">
<input type="file" name="uploadfiles" placeholder="파일 선택" multiple/><br/>
<input type="submit" value="업로드">
</form>
</body>
</html>
파일명: insert.jsp
[첨부(Attachments)]
insert.zip
[비고]
5, 6번(XML, Java 설정) 방식을 하는데 있어서 두 가지를 동시에 설정하면 충돌날 가능성이 있다.
주의해서 사용할 것을 권장한다.
5. 흔히 많이 알려진 XML 설정 방식
두 가지 셋팅을 해놓으면 간단하게 끝난다.
2가지 설정을 통해서 xml 셋팅이 간단하고 좋아보일 수도 있으나 라이브러리마다 셋팅의 양이 다양하고 복잡해지는 문제도 있다.
(지금 생각할 필요는 없음.)
1. root-context.xml (/src/main/webapp/WEB-INF/spring/root-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Root Context: defines shared resources visible to all other web components -->
<!-- MultipartResolver -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="100000000"/> <!-- 10MB-->
<property name="maxInMemorySize" value="100000000"/> <!-- 10MB-->
<!-- <property name="uploadTempDir" value=“0"/> -->
</bean>
<!-- 외부 경로를 찾는 방식은 동작하지 않음. -->
<!-- 외부 경로 찾는 방식으로 다운로드를 구현하려면, 다운로더를 하나 구현하는 것이 빠름. -->
<!-- 다운로드 경로는 servlet-context.xml에 넣어야 함. 이중 선언이 되어서 동작 안함. -->
<!--
<mvc:annotation-driven />
<mvc:resources mapping="/static2/**" location="file:///c:/temp/" />
<mvc:default-servlet-handler />
-->
</beans>
파일명: root-context.xml
[첨부(Attachments)]
root-context.zip
2. servlet-context.xml (/src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- 다운로드 외부 경로 -->
<resources mapping="/static2/**" location="file:///c:/temp/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<context:component-scan base-package="com.website.example" />
</beans:beans>
파일명: servlet-context.xml
[첨부(Attachments)]
servlet-context.zip
[Resources Mapping 관련]
밑줄 친 이 코드는 다운로드 위치(자원)를 맵핑해주는 명령어이다.
servlet-context.xml에 넣어야만 동작한다.
<mvn:resource......> 이렇게 해버리면, 돌아가지 않는다.
[유의할 점 - servlet-context.xml 이외의 파일에서 사용하고자 했을 때]
코드에 오류는 안 나는데, 동작이 안되는 당황스러운 일도 있다.
관리차원에서 파일을 하나 별도로 xml파일을 하나 생성해서, 구현을 해버리면 되어야 하는데 파일 자체에는 오류도 안 나고,
서버에 컴파일해서 올린 후 호출하면 오류가 뜨는 경우가 있다.
servlet-context.xml 파일이 수동 셋팅에서는 중요하다.
6. Java 설정 방식
RootWebConfig.java 파일 하나만 만들어놓으면, 추가 셋팅 할 필요없이 자동으로 인식해준다.
(인식해주는 이유는 "extends WebMvcConfigurerAdapter" 이 한 줄이 추가되서 그렇다.)
별도로 셋팅해줄 필요는 없다.
package com.website.example.common;
import java.io.File;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
@Import({FileConfig.class})
@ComponentScan(basePackages = {"com.website.example"})
@EnableWebMvc
//@ComponentScan(basePackages = {"com.local.example.beans", "com.local.example.advisor"})
public class RootWebConfig extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("file:c:" + File.separator + "temp/");
}
}
파일명: RootWebConfig.java
[첨부(Attachments)]
RootWebConfig.zip
package com.website.example.common;
import java.io.File;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@Configuration
public class FileConfig {
@Bean
public MultipartResolver multipartResolver() {
org.springframework.web.multipart.commons.CommonsMultipartResolver multipartResolver = new
org.springframework.web.multipart.commons.CommonsMultipartResolver();
multipartResolver.setMaxUploadSize(100000000);
multipartResolver.setMaxInMemorySize(100000000); // 10MB
return multipartResolver;
}
}
파일명: FileConfig.java
[첨부(Attachments)]
FileConfig.zip
7. FileController.java (/com/website/example)
package com.website.example;
import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
@Controller
public class FileController {
private static final Logger logger = LoggerFactory.getLogger(FileController.class);
private final String UPLOAD_PATH = "c:" + File.separator + "temp" + File.separator;
@RequestMapping(value = "/insert", method = RequestMethod.GET)
public String insert(Locale locale, Model model) {
logger.info("Welcome home! The client locale is {}.", locale);
return "insert";
}
@RequestMapping(value = "/fileupload",method = RequestMethod.POST)
public String upload(MultipartFile uploadfile, Model model) throws IOException{
logger.info("upload() POST 호출");
logger.info("파일 이름: {}", uploadfile.getOriginalFilename());
logger.info("파일 크기: {}", uploadfile.getSize());
String result = saveFile(uploadfile);
if(result !=null){ // 파일 저장 성공
model.addAttribute("result", result);
} else { // 파일 저장 실패
model.addAttribute("result","fail");
}
return "home";
}
@RequestMapping(value = "/fileupload2", method = RequestMethod.POST)
public String multiupload(@RequestParam("uploadfiles") MultipartFile[] file, Model model) throws IOException {
logger.info("Welcome multi file! The client locale is {}.", "Hello");
String result = "";
for(MultipartFile f : file){
result += saveFile(f);
}
return "home";
}
private String saveFile(MultipartFile file) throws IOException{
// 파일 이름 변경
UUID uuid = UUID.randomUUID();
String saveName = uuid + "_" + file.getOriginalFilename();
logger.info("saveName: {}",saveName);
String fileName = file.getOriginalFilename();
String contentType = file.getContentType();
long filesize = file.getSize();
// byte[] bytes = file.getBytes();
//System.out.println("파일명:" + fileName);
//System.out.println("컨텐츠 타입:" + contentType);
//System.out.println("파일크기:" + filesize);
// 저장할 File 객체를 생성(껍데기 파일)ㄴ
File saveFile = new File(UPLOAD_PATH, saveName); // 저장할 폴더 이름, 저장할 파일 이름
try {
file.transferTo(saveFile); // 업로드 파일에 saveFile이라는 껍데기 입힘
} catch (IOException e) {
e.printStackTrace();
return null;
}
return saveName;
}
}
파일명: FileController.java
[첨부(Attachments)]
FileController.zip
8. 출력 결과
동일한 결과를 볼 수 있다. 동일한 작업이기 때문이다.
차이점은 내부적인 관점에서 셋팅을 어떻게 했느냐 이런 문제라고 보겠다.
그림 3. (결과 1) 단일 업로드 모습
그림 4. (결과 1) 단일 업로드 모습 - 실제 파일 업로드 모습
그림 5. (결과 2) 다중 업로드 모습 - 실제 파일 업로드 모습
그림 6. (결과 2) 다중 업로드 모습 - 실제 파일 업로드 모습 - 폴더 모습
FileController.zip
* 맺음글(Conclusion)
Spring Framework 방식으로 Commons-io, Commons-FileUpload, CGLIB를 활용하여 업로드 프로그램을 구현하였다.
참고로 이 방식은 Spring Framework에서만 동작하는 방법이다.