본문 바로가기
Spring

[Spring] 파일 업로드(2) 한 개의 첨부파일 업로드

by moca7 2024. 10. 23.

 

 

ㅁ main.jsp 

 

 
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
   
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>    
<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"%>
   
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>    
<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 되지 않는다. 일단 내가 지정한 위치로 저장되는지를 확인한다.