ㅁ main.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h2>1. 한 개의 첨부파일 업로드 테스트</h2>
<h2>2. 다중 첨부파일 업로드 테스트</h2>
<h2>3. 비동기식으로 첨부파일 업로드 테스트</h2>
<h2>4. 첨부파일 목록 조회</h2>
</body>
</html>
- 메인페이지에 기능들을 작성한다.
- AJAX를 이용해서 비동기식으로 첨부파일 업로드를 해본다.
- 첨부파일 목록 조회도 AJAX를 이용해서 해본다.
- 기능 파악이 끝났으면 이제 기능 단위별로 서비스를 설계한다. (어떤 메소드가 필요할지)
ㅁ BoardService
package com.br.file.service;
import java.util.List;
import com.br.file.dto.AttachDto;
import com.br.file.dto.BoardDto;
public interface BoardService {
// 한 개의 첨부파일과 함께 게시글 등록
int insertOneFileBoard(BoardDto board, AttachDto attach);
// 다중 첨부파일과 함께 게시글 등록
int insertManyFileBoard(BoardDto board, List<AttachDto> list);
// 첨부파일 목록 조회
List<AttachDto> selectAttachList();
}
- List는 java.util.List를 import한다.
ㅁ BoarServiceImpl 수정
package com.br.file.service;
import java.util.List;
import org.springframework.stereotype.Service;
import com.br.file.dao.BoardDao;
import com.br.file.dto.AttachDto;
import com.br.file.dto.BoardDto;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class BoardServiceImpl implements BoardService {
private final BoardDao boardDao;
@Override
public int insertOneFileBoard(BoardDto board, AttachDto attach) {
return 0;
}
@Override
public int insertManyFileBoard(BoardDto board, List<AttachDto> list) {
return 0;
}
@Override
public List<AttachDto> selectAttachList() {
return null;
}
}
- add unimplemented methods로 BoardService 인터페이스의 추상메소드들을 임시로 구현해 놓는다.
ㅁ main.jsp 수정
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<style>
form > label {
font-size: 12px;
color: gray;
}
</style>
</head>
<body>
<script>
$(function(){ // 모든 요소가 만들어지고 나서 실행.
$("input[type=file]").on('change', function(evt){
const files = evt.target.files; // FileList {0:File, 1:File, ...}
console.log(files);
for(let i=0; i<files.length; i++){
if(files[i].size > 10 * 1024 * 1024) {
alert("첨부파일의 최대 크기는 10MB입니다.");
evt.target.value = ""; // 선택한 파일 초기화
}
}
})
})
</script>
<h2>1. 한 개의 첨부파일 업로드 테스트</h2>
<form action="${ contextPath }/board/insert1.do" method="post" enctype="multipart/form-data">
게시글 제목 : <input type="text" name="boardTitle"> <br>
게시글 내용 : <textarea name="boardContent"></textarea> <br>
첨부파일 : <input type="file" name="uploadFile"> <br>
<label>첨부파일 사이즈는 10MB 이하여야 됩니다.</label> <br><br>
<button type="submit">등록</button>
</form>
<h2>2. 다중 첨부파일 업로드 테스트</h2>
<h2>3. 비동기식으로 첨부파일 업로드 테스트</h2>
<h2>4. 첨부파일 목록 조회</h2>
</body>
</html>
- 한 개의 첨부파일 업로드는 메인 페이지에서 form요소로 진행한다.
- script 구문을 작성할건데 jQuery 구문을 사용하기 위해 cdn방식으로 jQuery 라이브러리 연동 구문을 <head>에 추가했다.
- 첨부파일을 막 받으면 안되고 용량 제한을 걸어야 한다.
10MB 이하로 용량 제한을 설정할 예정인데 사용자에게도 알려준다.
- 보통 가이드문구 쓸 때 그냥 label 태그를 쓰곤 한다. label 대신 div나 span을 써도 상관없다.
- label 태그에 css로 스타일을 입혔다.
- 첨부파일을 선택하는 순간 10mb를 초과하면 alert를 띄우면서 막는다.
화면단에서 유효성 검사를 할 수 있으면 하는게 좋다.
- 파일 선택이 되는 순간 script 코드가 수행되게할 수 있다.
type이 file인 input 요소에 change라는 이벤트가 발생되는 순간(파일이 선택되었을 때, 파일 선택이 취소되었을 때)
현재 선택된 파일의 용량을 알아낼 수 있다. 10mb를 초과하면 alert를 발생시킨다.
- <script>의 위치도 매우 중요하다.
만약 화면상의 모든 요소가 만들어지고 <script> 태그가 만들어지는것을 원한다면 $(function(){})을 쓰면 된다.
$(function(){})을 써야 요소가 다 만들어지고 script 구문이 실행된다. 그래야 요소를 선택할 수 있다.
- evt.target하면 현재 이벤트가 발생한 요소를 가리킨다.
이 input 요소에 선택된 파일들을 알아보고 싶으면 files 속성에 접근하면 된다.
- evt.target.files는 FileList 객체를 반환한다.
파일을 하나 선택했다면 0번 인덱스에 접근해서 알아내면 되고
파일을 여러개 선택했다면 반복문으로 순차적으로 선택된 파일 요소에 접근하면 된다.
- size 속성으로 파일 용량을 알 수 있다. 파일 용량은 바이트 단위다.
- evt.target.value = "";으로 input 요소의 value값을 초기화할 수 있다.
- 파일을 넘길 때는 무조건 post 방식으로 넘겨야 한다.
- 파일을 넘길 때는 무조건 enctype="multipart/form-data"를 써야 파일이 넘어간다.
enctype 속성을 안쓰면 파일명이 그냥 텍스트로만 넘어간다.
- 다른 사용자가 이 첨부파일을 다운받을 수 있으려면 이 파일을 어딘가에 저장시켜야 한다.
첨부파일에도 name 속성을 준다.
- 파일 선택을 누르고 10MB가 넘는 파일을 선택해본다.
- alert가 뜨고 선택한 파일이 초기화된다.
- 다시 10MB를 넘지 않는 파일을 선택해본다.
- 파일이 선택되면 콘솔에도 보인다. 파일명, 파일 사이즈 등을 볼 수 있다.
- 파일 사이즈는 바이트 단위다. 1.66kb가 1706 바이트다.
ㅁ BoardController
package com.br.file.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;
import com.br.file.dto.BoardDto;
import com.br.file.service.BoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequestMapping("/board")
@RequiredArgsConstructor
@Controller
public class BoardController {
private final BoardService boardService;
@PostMapping("/insert1.do")
public String insertOneFileBoard(BoardDto board, MultipartFile uploadFile) {
log.debug("board: {}", board);
log.debug("attach: {}", uploadFile);
return "redirect:/";
}
}
- @RequestMapping("/board") 어노테이션을 추가한다.
- 이제부터 요청시 전달값들은 매개변수로 받는다.
제목과 내용이 BoardDto 객체의 필드에 바로 담기게끔 input 요소의 name 속성값과 필드명을 일치시켰다.
- 파일은 컨트롤러의 매개변수에 MultipartFile 타입의 매개변수를 두면 된다.
이때도 매개변수명을 input 요소의 key값과 같게 하면 넘어온 파일 객체가 매개변수로 바로 전달된다.
- 각각의 매개변수에 값이 잘 담겼는지 확인용으로 로그를 출력한다.
그러려면 로그 객체가 필요하다. @Slf4j 어노테이션을 붙이면 Logger 객체를 얻어낼 필요 없이 바로 log로 쓸 수 있다.
- 제목과 내용을 입력하고 파일을 선택하고 등록 버튼을 누른다.
- 요청시 전달값이 담겨있지 않다. 파일뿐 아니라 제목과 내용도 주입되지 않았다.
- 스프링에서 multipart/form-data를 처리할 때 multipartResolver 설정이 누락되면 파일과 함께 전달되는 다른 폼 데이터도 제대로 처리되지 않아 DTO 객체에 값이 채워지지 않을 수 있다.
- 첨부파일 관련한 라이브러리가 필요하다.
라이브러리만 추가하면 되는게 아니고 또 내부적으로 사용하는 객체가 있는데,
그 객체를 빈으로 등록까지 해야 값들이 잘 담긴다.
ㅁ pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<modelVersion>4.0.0</modelVersion>
<groupId>com.br</groupId>
<artifactId>file</artifactId>
<name>05_Spring_FileUpload</name>
<packaging>war</packaging>
<version>1.0.0-BUILD-SNAPSHOT</version>
<properties>
<java-version>11</java-version>
<org.springframework-version>5.3.27</org.springframework-version>
<org.aspectj-version>1.9.19</org.aspectj-version>
<org.slf4j-version>2.0.7</org.slf4j-version>
</properties>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${org.springframework-version}</version>
<exclusions>
<!-- Exclude Commons Logging in favor of SLF4j -->
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- AspectJ (AOP를 위한 라이브러리)-->
<dependency> <!-- 원래 있던 것. 이게 있어야 "Advice 동작 시점"의 어노테이션들을 사용할 수 있다. -->
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<!-- 위빙 : Advice(공통로직)를 PointCut(핵심로직)에 로딩되도록 함 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${org.aspectj-version}</version>
<scope>runtime</scope>
</dependency>
<!-- Logging (logback) -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
<!-- <scope>test</scope> 이건 지워야 함 -->
</dependency>
<!-- (2) slf4j -->
<dependency> <!-- 기존 dependency 중 이것만 남기고 다 지운다.-->
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${org.slf4j-version}</version>
</dependency>
<!-- (3) log4jdbc -->
<dependency>
<groupId>org.bgee.log4jdbc-log4j2</groupId>
<artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
<version>1.16</version>
</dependency>
<!-- @Inject -->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<!-- Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!-- Lombok 라이브러리 추가 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
<!-- Jackson 라이브러리 추가 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.2</version>
</dependency>
<!-- db관련 라이브러리 -->
<!-- (1) ojdbc8 -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>23.2.0.0</version>
</dependency>
<!-- (2) spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- (3) mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.14</version>
</dependency>
<!-- (4) mybatis-spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
<!-- (5) commons-dbcp -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.9.0</version>
</dependency>
<!-- 파일 업로드 관련 라이브러리 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.9</version>
<configuration>
<additionalProjectnatures>
<projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
</additionalProjectnatures>
<additionalBuildcommands>
<buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
</additionalBuildcommands>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java-version}</source>
<target>${java-version}</target>
<compilerArgument>-Xlint:all</compilerArgument>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<configuration>
<mainClass>org.test.int1.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
- <dependecies> 태그 마지막 부분에 첨부파일과 관련한 라이브러리 두 개를 추가한다.
- commons-fileupload 검색. 1.5 버전 dependency 구문을 복사해서 붙여넣는다.
- commons-io 검색. 2.11.0 버전 dependency 구문을 복사해서 붙여넣는다. (Apache로 시작하는거)
- 라이브러리 추가했다고 끝이 아니고 객체를 빈으로 등록해야 한다.
직접 만든 객체가 아니라서 어노테이션을 붙이는 방식으로는 빈으로 등록할 수 없다.
xml이나 자바 방식으로 빈 등록해야 한다.
ㅁ root-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- mybatis 사용을 위한 빈 3개 -->
<bean class="org.apache.commons.dbcp2.BasicDataSource" id="dataSource" destroy-method="close">
<!-- <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" /> -->
<property name="driverClassName" value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy" />
<property name="url" value="jdbc:log4jdbc:oracle:thin:@localhost:1521:xe" />
<property name="username" value="sbatis" />
<property name="password" value="sbatis" />
</bean>
<bean class="org.mybatis.spring.SqlSessionFactoryBean" id="sqlSessionFactory">
<property name="configLocation" value="classpath:config/mybatis-config.xml" />
<property name="dataSource" ref="dataSource" />
</bean>
<bean class="org.mybatis.spring.SqlSessionTemplate" id="sqlSession">
<constructor-arg ref="sqlSessionFactory" />
</bean>
<!-- 트랜잭션 처리용 aop를 위한 구문 3개 -->
<!-- AOP를 이용한 트랜잭션 처리 -->
<!-- 1) 트랜잭션 매니저 빈으로 등록 (dataSource 객체 필요함) -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="txManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 2) 트랜잭션 Advice 등록 -->
<tx:advice transaction-manager="txManager" id="txAdvice">
<tx:attributes>
<tx:method name="*" /> <!-- pointcut의 모든 메소드에서 실행하겠다. (삽입, 수정, 삭제, 목록조회, 상세조회 등의 메소드가 있음) -->
<tx:method name="select*" read-only="true" /> <!-- 단, select로 시작하는 메소드는 실행하지 않는다. (삽입, 수정, 삭제 메소드만 실행됨) -->
</tx:attributes>
</tx:advice>
<!-- 3) AOP 등록 -->
<aop:config>
<aop:pointcut expression="execution(* com.br.sbatis.file.*Impl.*(..))" id="txPointcut" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut" />
</aop:config>
<!-- 파일 업로드를 위한 빈 등록 -->
<!-- 주의사항 : 빈 이름을 내 마음대로 하면 안되고 반드시 multipartResolver로 해야 한다. -->
<bean class="org.springframework.web.multipart.commons.CommonsMultipartResolver" id="multipartResolver">
<property name="maxUploadSizePerFile" value="10485760" />
<property name="maxUploadSize" value="104857600" />
<property name="defaultEncoding" value="UTF-8" />
</bean>
</beans>
- root-context.xml이 빈을 등록할 수 있는 파일이다. 서버 구동시 읽혀진다.
마이바티스 사용을 위한 빈과 트랜잭션 관련 구문이 쓰여있다.
그 아래에 파일 업로드를 위한 빈을 등록한다.
- 주의사항은 bean의 id를 반드시 multipartResolver로 해야 한다.
- class 속성은 CommonsMultipartResolver를 입력하고 ctrl + 스페이스바를 누르면 자동완성 된다.
- 이 객체를 우리가 쓰진 않지만 빈으로 등록을 해놔야 한다.
- <property>태그를 이용해서 setter 주입으로 CommonsMultipartResolver의 필드에 값을 주입한다.
- maxUploadSizePerFile. 파일 한 개당 용량 제한값을 설정한다. 바이트 단위로 써야 한다. 10mb는 10485760 바이트다.
화면단에서도 파일이 10MB 초과하면 담기지않게 설정하긴 했지만 혹시 모르니 또 설정한다.
- maxUploadSize. 다중 첨부 파일 업로드시 파일들의 총 용량을 100MB로 제한한다.
- defaultEncoding. 파일명에 대한 인코딩 처리. UTF-8로 인코딩한다.
- 제목과 내용을 작성하고 파일을 선택하고 등록 해본다.
- 제목, 내용, 파일 전부 정상적으로 넘어왔다.
- 스프링에선 위와 같이 MultipartFile 객체가 로그로 찍히지만 스프링부트에선 참조값만 찍힌다.
- 스프링과 스프링 부트는 서로 다른 MultipartFile 구현체를 사용한다.
스프링은 Apache Commons FileUpload 라이브러리의 CommonsMultipartFile을 기본으로 사용하고,
스프링 부트는 내장 서블릿 컨테이너(Tomcat)의 StandardMultipartFile을 기본으로 사용한다.
이 두 구현체의 toString() 메서드 구현이 다르기 때문에 로그에 출력되는 형태가 다르다.
- CommonsMultipartFile은 파일 정보를 포함하여 출력하고, StandardMultipartFile은 객체의 참조값만 출력한다.
- 제목과 내용은 입력하고 첨부 파일은 선택하지 않고 submit 해본다.
- 넘어갈 첨부파일 객체가 없어도 MultipartFile 객체가 null이 아니다. 객체가 생성은 되었다.
대신 비어있다. 비어있는 MultipartFile 객체가 넘어가게 된다.
- 파일을 선택하지 않아도 uploadFile이라는 key값으로 무언가가 넘어가게 된다. 대신 비어있다.
(text 타입의 input 요소를 입력하지 않고 넘기면 빈 문자열이 오듯이)
- 만약 uploadFile이라는 key값 자체가 넘어가지 않았다면 MultipartFile 객체는 null이었을 것이다.
- MultipartFile 객체는 생성되어 있다. 다만 파일은 존재하지 않는다.
파일을 선택하지 않으면 비어있는 파일 정보가 넘어간다.
==========================================================================
ㅁ BoardController
package com.br.file.controller;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;
import com.br.file.dto.BoardDto;
import com.br.file.service.BoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequestMapping("/board")
@RequiredArgsConstructor
@Controller
public class BoardController {
private final BoardService boardService;
@PostMapping("/insert1.do")
public String insertOneFileBoard(BoardDto board, MultipartFile uploadFile) {
log.debug("board: {}", board); // 잘 담겼는지 확인용 로그 출력
log.debug("attach: {}", uploadFile); // 잘 담겼는지 확인용 로그 출력
if(uploadFile != null && !uploadFile.isEmpty()) { // 첨부파일이 존재할 경우 => 업로드
// 전달된 파일 업로드 처리
// (1) 업로드할 폴더 (/upload/yyyyMMdd)
String filePath = "/upload/" + new SimpleDateFormat("yyyyMMdd").format(new Date());
File filePathDir = new File(filePath);
if(!filePathDir.exists()) { // 해당 경로의 폴더가 존재하지 않을 경우
filePathDir.mkdirs(); // 해당 폴더 만들기
}
// (2) 파일명 수정 작업
String originalFilename = uploadFile.getOriginalFilename(); // "xxxxx.jpg" | "xxxx.tar.gz" (파일 확장자가 2단계로 된 확장자도 있다)
// 원본명으로부터 확장자 추출하기
String originalExt = originalFilename.endsWith(".tar.gz") ? ".tar.gz"
: originalFilename.substring(originalFilename.lastIndexOf("."));
String filesystemName = UUID.randomUUID().toString().replace("-", "") + originalExt;
// (3) 업로드 (폴더에 파일 저장)
try {
uploadFile.transferTo(new File(filePathDir, filesystemName));
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return "redirect:/";
}
}
- uploadFile을 db에 기록하기 전에 먼저 내가 원하는 위치에 저장을시켜야 한다.
첨부파일을 저장시키고 db에 기록할 값을 뽑아서 AttachDto 객체에 담아서 넘겨야 한다.
일단 내가 원하는 경로로 파일이 저장되는지를 테스트한다.
- 첨부파일이 넘어오지 않을 수도 있다.
첨부파일이 있는지 없는지 비교해서 있을 때만 첨부파일 업로드를 진행한다.
- MultipartFile는 단일 파일을 처리할 때 사용하는 타입이다.
다중 파일을 처리하려면 MultipartFile[] 또는 List<MultipartFile>과 같은 배열이나 리스트 형태로 받을 수 있다.
(1) 업로드할 폴더 (/upload/yyyyMMdd)
- 첨부파일이 존재하면 원하는 위치에 upload한다.
어딘가에 저장해놔야 나중에 다시 본다거나 다운받을 수 있다.
- 전달된 파일을 upload하는 구문을 작성한다.
jsp/servlet으로 첨부파일 업로드할 때는 MultipartRequest로 변환하는 코드 1줄만으로 upload 처리가 됐었다.
대신 이때 파일명 수정작업을 하는 FileRenamePolicy를 구현하는 클래스를 만들었었다.
원본명에서 확장자를 뽑고 겹치지 않는 이름으로 파일명을 수정하는 코드를 작성했었다.
- 스프링에서 사용하는 라이브러리는 다른 방식이다.
- 먼저 업로드할 폴더에 대한 경로를 얻어야 한다.
jsp/servlet 때는 현재 프로젝트 안의 resources라는 소스폴더에 저장되게 했었다.
이번에는 외부 경로로 저장해본다.
- 현재 운영환경은 윈도우다. 윈도우에서 루트 디렉토리는 c드라이브다.
c드라이브 안에 upload 폴더가 만들어지고, 그 안에 날짜별로 폴더가 만들어지고 거기에 파일이 저장되게 한다.
- 지금은 윈도우 환경이라 c드라이브에 저장되지만 개발이 끝난 후 실제 서버에 배포하면 리눅스 환경이다.
그때는 리눅스의 루트 디렉토리에 파일이 저장될 것이다.
- /upload/yyyyMMdd처럼 /로 시작하는 경로는 절대 경로다.
Windows에서 이는 시스템의 루트 디렉터리, 즉 C 드라이브의 루트로 간주된다.
이 경로는 C:\upload\yyyyMMdd이다.
- /는 나중에 실제 서버에 배포하면 그 환경에서의 루트 디렉터리를 가리킨다.
- 한 폴더당 저장 가능한 파일의 개수도 제한이 있다. 6만개 정도.
그래서 이 코드가 실행되는 시점의 날짜별로 폴더가 생성되게끔 한다.
물론 만들어져 있을 수도 있다. 없다면 만들어지고 있으면 거기에 저장된다.
- SimpleDateFormat 객체를 생성하면 format 메소드를 제공한다.
format 메소드 호출시 Date 객체를 제시할 수 있다.
기본 생성자로 생성해서 제시하면 현재 시점의 날짜와 시간을 나타낸다
- java.util.Date를 import 한다.
- java.io.File를 import 한다.
- filePath의 경로에 해당하는 File 객체를 생성한다.
- 저 경로에 해당하는 폴더가 이미 있을 수도 있고 없을 수도 있다.
없으면 mkdirs() 메소드로 만들어야 한다.
(2) 파일명 수정 작업
- 전달받은 파일명 가지고 수정작업을 한다.
파일명이 중복될 수도 있고, 파일명에 한글, 공백, 특수문자가 있어서도 안된다.
- jsp/servlet 때는 파일명을 업로드한 날짜(밀리세컨초 환산) + 랜덤숫자 5자리 + 원본파일의 확장자로 수정했었다.
- uploadFile.getOriginalFilename()으로 파일의 원본명을 알아낼 수 있다.
- 원본명에서 마지막 .의 위치를 알아내서 substring으로 확장자를 추출했었다.
그런데 파일 확장자가 2단계에 걸친 확장자일 수도 있다.
이때는 마지막 .의 위치로 추출하면 온전한 위치가 아니다. 이런 케이스는 별도로 처리해줘야 한다.
- endsWith() 메소드를 사용해서 원본명이 ".tar.gz"으로 끝나면 true를 반환한다.
- 파일명은 겹치면 안된다. UUID로 랜덤값을 발생시킬 수 있다.
UUID.randomUUID().toString()를 호출하면 하이픈(대쉬, -)이 4개 섞인 32자리 문자를 반환한다.
(3) 업로드 (폴더에 파일 저장)
- 업로드할 폴더(filePathDir)에 업로드할 파일명(filesystemName)으로 upload가 되게 한다.
- uploadFile.transferTo(new File(filePathDir, filesystemName));
- 넘어온 MultipartFile 타입 객체인 uploadFile 변수의 transferTo 메소드 호출시
어떤 폴더에 어떤 이름으로 저장시킬지 제시하면 된다. File 객체로 제시한다.
다음과 같은 폴더에 다음과 같은 이름으로 이 파일을 변환해서 저장시키는 메소드다.
- 입출력이기 때문에 예외가 발생할 수 있다. try~catch로 감싸준다.
- 제목과 내용을 입력하고 첨부파일을 선택하고 등록한다.
- 아직 db에는 insert 되지 않는다. 일단 내가 지정한 위치로 저장되는지를 확인한다.