[SPRING]파일 업로드 처리-(1)

33483 단어 MultipartMultipart

스프링 부트로 파일을 업로드 하는 것은 아주 단순한 설정만으로도 가능합니다.
스프링 부트의 파일 업로드와 관련된 설정은
1) 별도의 파일 업로드 라이브러리(commons -fileload)등을 이용하는 경우,
2) Servlet 3 버전부터 추가된 자체적인 파일 업로드 라이브러리를 이용하는 방식
으로 구분할수 있습니다.

만약 프로젝트를 실행하는 WAS의 버전이 낮은 경우나 WAS가 아닌 환경에서 스프링부트 프로젝트를 실행한다면 별도의 라이브러리를 사용하는 것이 좋지만 서블릿 기반으로 설정해봅니다.

대부분의 웹 애플리케이션은 이미지 파일 등을 업로드할 때 섬네일을 만들어서 처리합니다.
->섬네일을 만들어서 목록이나 조회화면에서 보이도록 하고, 조회 화면에서는 섬네일을 클릭하면 원본 파일을 보이도록 작성합니다.

1. 파일 업로드를 위한 설정

스프링 부트 프로젝트를 내장된 Tomcat을 이용하여 실행한다면 별도의 추가적인 라이브러리 없이 application.properties 파일을 수정하는 것만으로 충분합니다.

  • spring.servlet.multipart.enabled : 파일 업로드 가능 여부를 선택합니다(true)
  • spring.servlet.multipart.location :업로드된 파일의 임시 저장 경로(/Users/YOUNJY/)
  • spring.servlet.multipart.max-request-size :한 번에 최대 업로드 가능 용량(30MB)
  • spring.servlet.multipart.max-file-size :파일 하나의 최대 크기(10MB)
  • part.upload.path = 업로드된 파일 저장

파일 업로드를 위한 컨트롤러와 화면 테스트

  • 실제 업로드된 파일 처리는 컨트롤러로 처리합니다.

  • 이에 관련해서 스프링에서는 MultipartFile 타입을 제공하므로 추가적인 처리 필요없이 바로 사용이 가능합니다.

  • 예제에서는 파일 업로드와 관련된 모든 작업은 Ajax방식으로 처리할 것이므로 업로드 결과에 대한 별도의 화면을 작성할 필요가 없습니다.

  • 모든 업로드 결과는 JSON 형태로 제공하도록 작성합니다.

  • 컨트롤러는 controller 패키지를 생성하고, UploadController 클래스로 추가합니다.

@RestController
@Log4j2
public class uploadController{

	@PostMapping("/uploadAjax")
    public void uploadFile(MultipartFile[] uploadFiles){	
    //MultipartFile은 단건만 배열로 설정하면 다수의 파일을 받을 수있습니다.
	//배열을 활용하면 동시에 여러개의 파일 정보를 처리할 수 있으므로 화면에서 여러개의 파일을 동시에 업로드 할 수 있습니다.
    
    for(MultipartFile uploadFile : uploadFiles){
    	//브라우저에 따라 업로드하는 파일의 이름은 전체경로일 수도 있고(Internet Explorer),
		//단순히 파일의 이름만을 의미할 수도 있습니다.(chrome browser)
        String originalName = uploadFile.getOriginalFilename();//파일명:모든 경로를 포함한 파일이름
        String fileName = originalName.subString(originalName.lastIndexOf("//")+1);
        //예를 들어 getOriginalFileName()을 해서 나온 값이 /Users/Document/bootEx 이라고 한다면 
        //"마지막으로온 "/"부분으로부터 +1 해준 부분부터 출력하겠습니다." 라는 뜻입니다.따라서 bootEx가 됩니다.
        log.info("fileName" + fileName);
        }//end for
        }
        }

테스트를 위한 컨트롤러와 화면

  • 실제 업로드는 브라우저상에서 JQuery로 처리할 것이므로 conroller 패키지에 UploadTestController를 추가하고 Get방식으로 화면을 볼 수 있도록 합니다.
@Controller
public class UploadTestController{
	
    @GetMapping("/uploadEx")
    public void uploadEx(){
    }
    }
  • 이후 templates 폴더에는 uploadEx.html 파일을 추가하여 화면 내용을 구성합니다.
  • Ajax로 파일 업로드를 하기 위해서는 가사의 Form객체를 만들어서 사용합니다.
  • FormData 라는 객체로 전송하려는 내용을 추가할 수 있는데 파일 데이터도 포함이 가능합니다.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<input name="uploadFiles" type="file" multiple>
<button class="uploadBtn">Upload</button>
 <script src="https://code.jquery.com/jquery-3.5.1.min.js"
            integrit="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
            crossorigin="anonymous">
    </script>
    <script>
        $('.uploadBtn').click(function( ) {

            var formData = new FormData();	//FormData 객체 생성

            var inputFile = $("input[type='file']");	
            //input 태그의 type이 file인것을 찾아서 inputFile이라는 변수로 지정

            var files = inputFile[0].files;
            //files : 선택한 모든 파일을 나열하는 FileList 객체입니다.
            //multiple 특성을 지정하지 않닸다면 두 개 이상의 파일을 포함하지 않습니다.

            for (var i = 0; i < files.length; i++) {
                console.log(files[i]);
                formData.append("uploadFiles", files[i]);//키,값으로 append 
            }

           //실제 업로드 부분
           //upload ajax
           $.ajax({
               url: '/uploadAjax',	//경로
               processData: false,	//기본값은 true
               //ajax 통신을 통해 데이터를 전송할 때, 기본적으로 key와 value값을 Query String으로 변환해서 보냅니다.
               contentType: false,	// multipart/form-data타입을 사용하기위해 false 로 지정합니다.
               data: formData,
               type: 'POST',
               dataType:'json',
               success: function(result){
                   //나중에 화면 처리
                   console.log(result);
               },
               error: function(jqXHR, textStatus, errorThrown){	//오류 메시지 판정
                   console.log(textStatus);
               }

           }); //$.ajax
       });

    </script>

</body>
</html>


2.업로드된 파일의 저장

  • 파일이 업로드 되는 것을 확인했다면 그 다음은 실제로 업로드 된 파일을 저장해야 합니다.
  • 스프링 자체에서 제공하는 FileCopyUtils를 이용할 수도 있고,MultipartFile 자체에도 transferTo()를 사용하면 간단하게 파일을 저장할 수 있습니다.
  • 파일을 저장할 떄 경로는 설정 파일을 통해서 저장하고 사용할 수 있도록 application.properties에 별도의 설정값을 추가하고 UploadController에서 이 설정값을 이용하도록 작성합니다.
  • 현재 프로젝트의 Group이름+upload+path 라고 지정후 원하는 파일의 경로를 적어줍니다.
@RestController
@Log4j2
public class UploadController{

	<--------추가된 부분--------->
	@Value("${part4.upload.path})//application.properties의 변수
    private String uploadPath;
    //@Value를 import할때 springframwork.beans.factory.annotation.Value;를 선택!
    <--------추가된 부분--------->

    
    @PostMapping("/uploadAjax)
    public void uploadFile(MultipartFile[] uploadfiels){
    //생략
    }

파일을 저장하는 단계에서 고려해야할 사항

  • 업로드된 확장자가 이미지만 가능하도록 검사해야합니다(첨부파일을 이용한 원격 셀)
    -확장자체크!
  • 동일한 이름의 파일이 업로드 된다면 기존 파일을 덮어쓰는 문제
  • 업로드된 파일을 저장하는 폴더의 용량

동일한 이름의 파일 문제

  • 만약 첨부파일의 이름이 같은 경우에는 기존의 파일이 사라지고 새로운 파일로 변경되기 때문에 문제가 발생할 수 있습니다.
  • 이를 막기 위해서는 고유한 이름을 생성해서 파일이름으로 사용해야합니다.
  • 가장 많이 사용하는 방식은
    -1)시간 값을 파일 이름에 추가.
    -2)UUID를 이용하여 고유한 값을 만들어서 사용하는 방식.(java.util 패키지의 UUID를 이용)
    (파일 이름은 ' UUID값_파일명' 형태로 저장합니다.)
    -3)이렇게 하면 실제 폴더에는 UUID값이 파일이름으로 사용되기 때문에 동일한 이름의 파일이만 다른 이름을 부여할 수 있어서 덮어쓰는 문제의 발생을 막습니다.

동일한 폴더에 너무 많은 파일

  • 업로드 되는 파일들을 동일한 폴더에 넣는다면 너무 많은 파일이 쌓이게 되고 성능이 저하됩니다.
  • 무엇보다 운영체제에 따라 하나의 폴더에 넣을 수 있는 파일의 수에 대한 제한이 있습니다.
    (FAT32 방식은 65,354개라는 제한이 있습니다.)
  • 일반적으로 가장 많이 쓰는 방법은 파일이 저장되는 시점에 '년/월/일' 폴더를 따로 생성해서 한 폴더에 너무많은 파일이 쌓이지 않도록 하는 것입니다.

파일의 확장자 체크

  • 첨부파일을 이용하여 '쉘 스크립트'파일 등을 업로드해서 공격하는 기법들도 있기 때문에 브라우저에서 파일을 업로드하는 순간이나 서버에서 파일을 저장하는 순간에도 이를 검사해주는 과정을 거쳐줘야 합니다.
  • 이 처리는 MultipartFile에서 제공하는 getContentType()를 이용해서 처리할 수 있습니다.
@RestController
@Log4j2
public class UploadController {

    @Value("${part4.upload.path}") 
    private String uploadPath;

    @PostMapping("/uploadAjax")
    public void uploadFile(MultipartFile[] uploadFiles) {
    
     for(MultipartFile uploadFile : uploadFiles){
     
     <---------추가----------->
     if(uploadfile.getContentType().startWith("image") == false{
     	log.warn("this file is not image type");
        return;
        }
     </---------추가-----------/>
        String originalName = uploadFile.getOriginalFilename();
        String fileName = originalName.subString(originalName.lastIndexOf("//")+1);
        
        log.info("fileName" + fileName);
     <---------추가----------->

        //날짜 폴더 생성
        String folderPath = makeFolder();
        //UUID
        String uuid = UUID.randomUUID().toString();
        //저장할 파일 이름 중간에 "_"를 이용하여 구분
        String saveName = uploadPath + File.separator + folderPath +File.separator + uuid + "_" + fileName;
        
        Path savePath = Paths.get(saveName);
        //Paths.get() 메서드는 특정 경로의 파일 정보를 가져옵니다.(경로 정의하기)
        
        try{
        	uploadFile.transferTo(savePath)
            //uploadFile에 파일을 업로드 하는 메서드 transferTo(file)
        } catch (IOException e) {
             e.printStackTrace();
             //printStackTrace()를 호출하면 로그에 Stack trace가 출력됩니다.
        }
      </---------추가-----------/>
        
        }//end for
        }
      <---------추가----------->
      private String makeFolder(){
      
      	String str = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
        //LocalDate를 문자열로 포멧
        String folderPath = str.replace("/". File.separator);
        //만약 Data 밑에 exam.jpg라는 파일을 원한다고 할때,
        //윈도우는 "Data\\"eaxm.jpg", 리눅스는 "Data/exam.jpg"라고 씁니다.
        //그러나 자바에서는 "Data" +File.separator + "exam.jpg" 라고 쓰면 됩니다.
        
        //make folder ==================
        File uploadPathFoler = new File(uploadPath, folderPath);
        //File newFile= new File(dir,"파일명");
        //->부모 디렉토리를 파라미터로 인스턴스 생성
        
        if(uploadPathFolder.exists() == false){
	        uploadPathFoler.mkdirs();
            //만약 uploadPathFolder가 존재하지않는다면 makeDirectory하라는 의미입니다.
            //mkdir(): 디렉토리에 상위 디렉토리가 존재하지 않을경우에는 생성이 불가능한 함수
			//mkdirs(): 디렉토리의 상위 디렉토리가 존재하지 않을 경우에는 상위 디렉토리까지 모두 생성하는 함수
           }
           return folderPath;
           }
        }
    
  • Paths.get()메서드의 활용

프로젝트를 실행하고 위의 코드를 이용하면 지정한 폴더에 '년/월/일' 폴더가 생성되면서 파일들이 업로드 되는 것을 확인할 수 있습니다.

getContentType() 메서드의 결과물이 궁금해서 구글링....

  • MIME 타입 이러가 한다.
  • MIME 타입이란 클라이언트에게 전송된 문서의 다양성을 알려주기 위한 메커니즘입니다.
  • 웹에서 파일의 확장자는 별 의미가 없지만, 각 문서와 함께 올바른 MIME 타입을 전송하도록, 서버가 정확히 설정하는 것이 중요합니다.
  • 일반 적인 구조
    -type/subtype
    -MIME 타입의 구조는 매우 간단합니다; '/'로 구분된 두 개의 문자열인 타입과 서브타입으로 구성됩니다. 스페이스는 허용되지 않습니다. type은 카테고리를 나타내며 개별(discrete) 혹은 멀티파트 타입이 될 수 있습니다.
    -subtype 은 각각의 타입에 한정됩니다.

MIME 타입은 대소문자를 구분하지는 않지만 전통적으로 소문자로 쓰여집니다.

  • 개별타입 종류

좋은 웹페이지 즐겨찾기