Keep going

파일 업로드 상세 처리 본문

Records/Spring Framework

파일 업로드 상세 처리

코딩천재홍 2021. 3. 28. 14:07

 

22.1 파일의 확장자나 크기의 사전 처리

최근 포털에서도 특정한 확장자를 제외한 파일들의 업로드를 제한하는 경우가 많은데, 이는 첨부파일을 이용하는 웹 공격을 막기 위해서 행해지는 조치이다.

실습에서는 확장자가 (exe, sh, zip)등의 경우에는 업로드를 제한하고, 특정 크기 이상의 파일은 업로드할 수 없도록 제한하는 처리를 JavaScript로 처리한다.

 

 

uploadAjax.jsp에 파일의 확장자와 크기를 설정하고, 이를 검사하는 함수 checkExtension()을 작성해서 적용한다.

 

    • uploadAjax.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Upload with Ajax</h1>
<div class='uploadDiv'>
    <input type='file' name='uploadFile' multiple>
</div>
<button id='uploadBtn'> Upload </button>
 
<script src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
 
<script>
$(document).ready(function() {
        var regex = new RegExp("(.*?)\.(exe|sh|zip|alz)$");
        var maxSize = 5242880//5MB
        
        function checkExtension(fileName, fileSize) {
            if(fileSize >= maxSize) {
                alert("파일 사이즈 초과");
                return false;
            }
            
            if(regex.test(fileName)) {
                alert("해당 종류의 파일은 업로드할 수 없습니다.");
                return false;
            }
            return true;
        }
        
    $("#uploadBtn").on("click"function(e){
        var formData = new FormData();
        var inputFile = $("input[name='uploadFile']");
        var files = inputFile[0].files;
        console.log(files);
        
        //add filedate to formdata
        for(var i=0; i<files.length; i++) {
            if(!checkExtension(files[i].name, files[i].size)) {
                return false;
            }
            formData.append("uploadFile", files[i]);
        }
        
        $.ajax({
            url: '/uploadAjaxAction',
            processData : false,
            contentType: false,
            data: formData,
            type: 'POST',
            success: function(result) {
                alert("Uploaded");
            }
        });
    });
});
</script>
 
</body>
</html>
cs

첨부파일을 업로드하면 for 루프에서 checkExtension( )을 호출해서 확장자와 파일의 크기를 체크하게 된다.

 

 

 

  • 중복된 이름의 첨부파일 처리

    • 첨부파일을 저장할 때 신경 쓰이는 것은 크게 두가지로 1) 중복된 이름의 파일 처리와 2) 한 폴더 내에 너무 많은 파일의 생성문제다.
    • 1)의 경우는 현재 시간을 밀리세컨드까지 구분해서 파일 이름을 생성해서 저장하거나 UUID를 이용해서 중복이 발생할 가능성이 거의 없는 문자열을 생성해서 처리한다.
    • 2)의 경우는 하나의 폴더에 생성될 수 있는 파일의 개수에 대한 문제인데, 한 폴더에 너무 많은 파일이 이쓴 경우 속도와 저하와 개수의 제한 문제가 생기는 것을 방지해야 한다.
    • 이에 대한 해결책으로 '년/월/일' 단위의 폴더를 생성해서 파일을 저장하는 것이다.

 

 

  • 년/월/일 폴더의 생성

    • 첨부파일을 보관하는 폴더를 생성하는 작업은 한 번에 폴더를 생성하거나 존재하는 폴더를 이용하는 방식을 사용한다.
    • java.io.File에 존재하는 mkdirs()를 이용하면 필요한 상위 폴더까지 한 번에 생성할 수 있으므로 간단히 처리할 수 있다.

 

  • UploadController 
private String getFolder() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date date = new Date();
        String str = sdf.format(date);
        return str.replace("-",  File.separator);
    }
    
    @PostMapping("/uploadAjaxAction")
    public void uploadAjaxPost(MultipartFile[] uploadFile) {
        log.info("update ajax post.........");
        String uploadFolder = "C:\\upload";
        
        //make folder
        File uploadPath = new File(uploadFolder, getFolder());
        log.info("upload path : " + uploadPath);
        
        if(uploadPath.exists() ==false) {
            uploadPath.mkdirs();
        } //make folder
        
        for(MultipartFile multipartFile : uploadFile) {
            log.info("-------------");
            log.info("Upload File Name : " + multipartFile.getOriginalFilename());
            log.info("Upload File Size : " + multipartFile.getSize());
            String uploadFileName = multipartFile.getOriginalFilename();
            
            //IE has file path
            uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\"+ 1);
            log.info("only file name : " + uploadFileName);
            File saveFile = new File(uploadPath, uploadFileName);
            
            try {
                multipartFile.transferTo(saveFile);
                
            }catch(Exception e) {
                log.error(e.getMessage());
            }
        }
    }
cs

- getFolder()는 오는 날짜의 경로를 문자열로 생성한다. 생성된 경로는 폴더 경로로 수정된 뒤에 반환한다.

- uploadAjaxPost()에서는 해당 경로가 있는지 검사하고, 폴더를 생성한다. 이후 생성된 폴더로 파일을 저장하게 한다.

- 위와 같이 폴더를 생성한 후 uploadPath 경로에 파일을 저장헥 되면 자동으로 폴더가 생성되면서 파일이 저장되는 것을 볼 수 있다.

 

 

  • 중복 방지를 위한 UUID 적용

    • 파일 이름을 생성할 때 동일한 이름으로 업로드 되면 기존 파일을 지우게 되므로 java.util.UUID의 값을 이용해서 처리한다.
  • UploadController
package org.zerock.controller;
 
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
 
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.multipart.MultipartFile;
 
import lombok.extern.log4j.Log4j;
 
@Controller
@Log4j
public class UploadController {
    @GetMapping("/uploadForm")
    public void uploadForm() {
        log.info("upload form");
    }
    
    @PostMapping("/uploadFormAction")
    public void uploadFormPost(MultipartFile[] uploadFile, Model model) {
        
        String uploadFolder = "C:\\upload";
        
        for(MultipartFile multipartFile : uploadFile) {
            log.info("--------------------");
            log.info("Upload File Name : " + multipartFile.getOriginalFilename());
            log.info("Upload File size : " + multipartFile.getSize());
            
            File saveFile = new File(uploadFolder, multipartFile.getOriginalFilename());
            
            try {
                multipartFile.transferTo(saveFile);
                
             } catch(Exception e) {
                 log.error(e.getMessage());;
             }
        }
    }
    
    @GetMapping("/uploadAjax"
    public void uploadAjax() {
        log.info("upload ajax");
    }
    
    private String getFolder() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date date = new Date();
        String str = sdf.format(date);
        return str.replace("-",  File.separator);
    }
    
    @PostMapping("/uploadAjaxAction")
    public void uploadAjaxPost(MultipartFile[] uploadFile) {
        log.info("update ajax post.........");
        String uploadFolder = "C:\\upload";
        
        //make folder
        File uploadPath = new File(uploadFolder, getFolder());
        log.info("upload path : " + uploadPath);
        
        if(uploadPath.exists() ==false) {
            uploadPath.mkdirs();
        } //make folder
        
        for(MultipartFile multipartFile : uploadFile) {
            log.info("-------------");
            log.info("Upload File Name : " + multipartFile.getOriginalFilename());
            log.info("Upload File Size : " + multipartFile.getSize());
            String uploadFileName = multipartFile.getOriginalFilename();
            
            //IE has file path
            uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\"+ 1);
            log.info("only file name : " + uploadFileName);
            
            UUID uuid = UUID.randomUUID();
            uploadFileName = uuid.toString() + "_" + uploadFileName;
            File saveFile = new File(uploadPath, uploadFileName);
            
            try {
                multipartFile.transferTo(saveFile);
                
            }catch(Exception e) {
                log.error(e.getMessage());
            }
        }
    }
}
cs

- 첨부파일은 randomUUID()를 이용해서 임의의 값을 생성한다.

- 생성된 값은 원래의 파일 이름과 구분할 수 있도록 중간에 '_'를 추가한다. 나중에 앞에서부터 '_'를 기준으로 분리하면 원래의 파일 이름을 파악할 수 있다.

 


22.2 섬네일 이미지 생성

- 일반 파일과 이미지 파일을 구분해야 한다. 

- 이미지 파일의 경우에는 화면에 보여지는 작은 이미지를 생성하는 추가적인 처리이다.

- 만일 용량이 큰 파일을 섬네일 처리하지 않는다면 모바일과 같은 환경에서 많은 데이터를 소비해야만 하므로 이미지의 경우는 특별한 경우가 아니라면 섬네일을 제작해야만 한다.

 

 

섬네일 제작하는 방법

  • ImageIO (JDK1.4부터)
  • ImgScalr 라이브러리
  • Thumbnailator 라이브러리 (실습에서 사용)

 

UploadController에서는 다음과 같은 단계를 이용해서 섬네일을 생성한다.

  • 업로드된 파일이 이미지 종류의 파일인지 확인
  • 이미지 파일의 경우에는 섬네일 이미지 생성 및 저장

 

porm.xml에 thumbnailator 라이브러리 포함

 

 

  • 이미지 파일의 판단

    • 화면에서 약간의 검사를 통해 업로드되는 파일의 확장자를 검사하기는 하지만, Ajax로 사용하는 호출은 반드시 브라우저만을 통해서 들어오는 것이 아니므로 확인할 필요가 있다.
    • 서버에 업로드 된 파일은 조금 시간이 걸려도 파일 자체가 이미지인지를 정확히 체크한 후 저장하는 것이 좋다.

 

  • checkImageType( ) 메서드
private boolean checkImageType(File file) {
        try {
            String contentType = Files.probeContentType(file.toPath());
            return contentType.startsWith("image");
            
        }catch(IOException e) {
            e.printStackTrace();
        }
        return false;
    }
cs

 

 

  • UserController
package org.zerock.controller;
 
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
 
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;
 
import lombok.extern.log4j.Log4j;
import net.coobird.thumbnailator.Thumbnailator;
 
@Controller
@Log4j
public class UploadController {
    @GetMapping("/uploadForm")
    public void uploadForm() {
        log.info("upload form");
    }
    
    @PostMapping("/uploadFormAction")
    public void uploadFormPost(MultipartFile[] uploadFile, Model model) {
        
        String uploadFolder = "C:\\upload";
        
        for(MultipartFile multipartFile : uploadFile) {
            log.info("--------------------");
            log.info("Upload File Name : " + multipartFile.getOriginalFilename());
            log.info("Upload File size : " + multipartFile.getSize());
            
            File saveFile = new File(uploadFolder, multipartFile.getOriginalFilename());
            
            try {
                multipartFile.transferTo(saveFile);
                
             } catch(Exception e) {
                 log.error(e.getMessage());;
             }
        }
    }
    
    @GetMapping("/uploadAjax"
    public void uploadAjax() {
        log.info("upload ajax");
    }
    
    private String getFolder() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date date = new Date();
        String str = sdf.format(date);
        return str.replace("-",  File.separator);
    }
    
    private boolean checkImageType(File file) {
        try {
            String contentType = Files.probeContentType(file.toPath());
            return contentType.startsWith("image");
            
        }catch(IOException e) {
            e.printStackTrace();
        }
        return false;
    }
    
    @PostMapping("/uploadAjaxAction")
    public void uploadAjaxPost(MultipartFile[] uploadFile) {
        log.info("update ajax post.........");
        String uploadFolder = "C:\\upload";
        
        //make folder
        File uploadPath = new File(uploadFolder, getFolder());
        log.info("upload path : " + uploadPath);
        
        if(uploadPath.exists() ==false) {
            uploadPath.mkdirs();
        } //make folder
        
        for(MultipartFile multipartFile : uploadFile) {
            log.info("-------------");
            log.info("Upload File Name : " + multipartFile.getOriginalFilename());
            log.info("Upload File Size : " + multipartFile.getSize());
            String uploadFileName = multipartFile.getOriginalFilename();
            
            //IE has file path
            uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\"+ 1);
            log.info("only file name : " + uploadFileName);
            
            UUID uuid = UUID.randomUUID();
            uploadFileName = uuid.toString() + "_" + uploadFileName;
            
            try {
                File saveFile = new File(uploadPath, uploadFileName);
                
                multipartFile.transferTo(saveFile);
                //check image type file
                
                if(checkImageType(saveFile)) {
                    FileOutputStream thumbnail = new FileOutputStream(new File(
                            uploadPath, "s_" + uploadFileName));
                    Thumbnailator.createThumbnail(multipartFile.getInputStream(),
                            thumbnail, 100100);
                    thumbnail.close();
                }
            }catch(Exception e) {
                log.error(e.getMessage());
            }
        }
    }
}
cs

- Thumbinator는 InputStream과 java.io.File 객체를 이용해서 파일을 생성할 수 있고, 뒤에 사이즈에 대한 부분을 파라미터로 width와 height를 지정할 수 있다.

- 이제 /uploadAjaxx를 이용해서 이미지 파일을 업로드하면 원본 파일은 그대로 저장되고, 파일 이름이 's_'로 시작하는 섬네일 파일이 생성되는 것을 볼 수 있다. 반면 일반 파일의 경우는 그냥 파일만 업로드 되는 것을 볼 수 있다.

 


22.3 업로드된 파일의 데이터 반환

현재 Ajax 를 이용해서 파일을 업로드했지만, 아직 브라우저 쪽에 아무런 데이터도 전달하지 않았기 때문에 브라우저에서는 어떠한 피드백도 받을 수 없는 상황이다.

서버에서 Ajax이 결과로 전달해야 하는 데이터는 업로드된 파일의 경로가 포함된 파일의 이름이다.

섬네일 의 경우에는 's_'로 시작한다는 규칙만 알고 있으면 필요할 때 사용할 수 있다.

 

 

브라우저로 전송해야 하는 데이터는 다음과 같은 정보를 포함하도록 설계한다.

  • 업로드된 파일의 이름과 원본 파일의 이름
  • 파일이 저장된 경로
  • 업로드된 파일이 이미지인지 아닌지에 대한 정보

이에 대한 모든 정보를 처리하는 방법은 1)업로드된 경로가 포함된 파일 이름을 반환하는 방식과  2)별도의 객체를 생성해서 처리하는 방법 을 고려할 수 있다.

 

 

por.xml에 jackson-databind 라이브러리 포함

 

 

  • AttachFileDTO 클래스

    • 첨부파일의 정보들을 저장하는 클래다.

    • AttachFileDTO 클래스에는 원본 파일의 이름(fileName), 업로드 경로(uploadPath), UUID값(uuid), 이미지 여부(image) 정보를 하나로 묶어서 전달하는 용도로 사용한다.

package org.zerock.domain;
 
import lombok.Data;
 
@Data
public class AttachFileDTO {
    private String fileName;
    private String uploadPath;
    private String uuid;
    private boolean image;
}
cs

 

 

UserController는 AttachFileDTO의 리스트를 반환하는 구조로 변경해야 한다.

 

  • UploadController
@PostMapping(value="/uploadAjaxAction",
            produces= MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    public ResponseEntity<List<AttachFileDTO>> 
       uploadAjaxPost(MultipartFile[] uploadFile) {
        List<AttachFileDTO> list = new ArrayList<AttachFileDTO>();
        String uploadFolder= "C:\\upload";
        
        String uploadFolderPath = getFolder();
        //make folder
        
        File uploadPath = new File(uploadFolder, uploadFolderPath);
        if(uploadPath.exists()==false) {
            uploadPath.mkdirs();
        }
        
        for(MultipartFile multipartFile : uploadFile) {
            AttachFileDTO attachDTO = new AttachFileDTO();
            String uploadFileName = multipartFile.getOriginalFilename();
            
            uploadFileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\"+ 1);
            log.info("only file name : " + uploadFileName);
            attachDTO.setFileName(uploadFileName);
            
            UUID uuid = UUID.randomUUID();
            
            uploadFileName = uuid.toString() + "_" + uploadFileName;
            
            try {
                File saveFile = new File(uploadPath, uploadFileName);
                multipartFile.transferTo(saveFile);
                
                attachDTO.setUuid(uuid.toString());
                attachDTO.setUploadPath(uploadFolderPath);
                
                if(checkImageType(saveFile)) {
                    attachDTO.setImage(true);
                    FileOutputStream thumbnail = new FileOutputStream(
                            new File(uploadPath, "s_" + uploadFileName));
                    Thumbnailator.createThumbnail(multipartFile.getInputStream(), 
                            thumbnail, 100100);
                    thumbnail.close();
                }
                list.add(attachDTO);
            } catch(Exception e) {
                e.printStackTrace();
            }
        }
        return new ResponseEntity<List<AttachFileDTO>>(list, HttpStatus.OK);
    }
cs

- uploadAjaxPost()는 기존과 달리 ResponseEntity<List<AttachFileDTO>>를 반환하는 형태로 수정하고, JSON 데이터를 반환하도록 변경된다.

- 내부에서는 각 파일에 맞게 AttachFileDTO를 생성해서 전달하는 구조로 변경된다.

 

 

  • 브라우저에서 Ajax 처리

    • /uploadAjax에서 결과 데이터를 JavaScript를 이용해서 반환된 정보를 처리하도록 수정한다.

 

  • uploadAjax.jsp
$.ajax({
    url: '/uploadAjaxAction',
    processData : false,
    contentType: false,
    data: formData,
    type: 'POST',
    dataType:'json',
    success: function(result) {
                
        console.log(result);
    }
});
cs

-Ajax를 호출했을 때의 결과 타입은 'json'으로 변경하고, 결과를 찍도록 변경했다.

 

 


출처 : 코드로 배우는 스프링 웹 프로젝트 [구멍가게 코딩단]

'Records > Spring Framework' 카테고리의 다른 글

브라우저에서 섬네일 처리  (0) 2021.03.28
파일 업로드 방식  (0) 2021.03.28
댓글과 댓글 수에 대한 처리  (0) 2021.03.26
스프링에서 트랜잭션 관리  (0) 2021.03.22
AOP라는 패러다임  (0) 2021.03.22
Comments