springframework

파일업로드의 고민점, (지속적으로 추가 예정..)

Jungsoomin :) 2020. 9. 7. 04:43

일단, 시작은 어떤 api를 이용할 것인가가 시작이 되는 것 같다.

  1. commons-fileupload + commons-io + CommonsMultipartResolver 
  2. servlet3.0 이상에서 제공하는 <multipart-config> + StandardServletMultipartResolver

두 조합이다.

 

그 다음은 콜을 form 에서 할지 Ajax로 할지의 결정이다.

  1. 브라우저의 제한이 없어야할 경우 form 태그 사용 > 페이지와 업로드가 동시에 이루어지는가, iframe을 이용해서 처리할 것인가.
  2. 첨부파일을 별도로 처리할 경우 Ajax 사용 > <input type="file"> 태그를 이용해 Ajax 콜, JQuery 라이브러리나, HTML의 Drag And Drop 기능

서버 쪽에서의 처리후 응답을 HTML 로 할지 JSON 으로 할지의 고민.

 


  1. form 을 사용한다면 enctype="multipart/form-data" 를 추가해줘야겠고.
  2. Ajax 를 사용한다면 $.ajax의 contentType , processData 는 false가 되어야한다. 그리고 가상의 Form인 FormData 객체를 사용하게 된다.
  3. Ajax 콜을 할시 input:file 객체의 값이 여러개라면 FileList로 File객체의 배열로 오기떄문에 하나의 파일 객체는 File[index] 로 정의할 수 있다. 이를 FormData 객체에 append(키, 값) 시켜 넘겨보내면 
  4. 서버에서는 MultipartFile 객체로 넘겨받을 수 있다.

이후 중요한 고민점들을 생각해보면..

  1. 동일한 이름으로 파일이 업로드 되었을때 기존파일이 삭제되는 문제. > java.util.UUID를 사용하여 파일이름에 구분자를 넣고 저장
  2. 이미지파일의 경우 파일용량이 크다면 섬네일 이미지를 생성해야하는 문제
  3. 이미지파일과 일반 파일을 구분하여, 일반파일은 아이콘 다운로드 , 이미지는 조회 하도록 처리해야하는 문제
  4. 첨부파일 공격에 대비하기위한 업로드파일의 확장자 제한 문제.

이다.

 

이제부터 추후 업데이트하며 기술.


확장자와 크기

1.  exe,zip.alz,sh 등 업로드 제한 > 자바스크립트 정규표현식 사용. >RegExp 객체이용

2. 크기제한 > 변수에 크기를 저장하고 함수에 파라미터로 넘어오는 file객체의 사이즈와 비교.

$(function(){
	var regex = new RegExp("(.?)\.(exe|sh|zip|alz)$");
	var maxSize = 5242880;
	
	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:file[name='uploadFile']");
		
		var files = inputFile[0].files;
		
		console.log(files);
		
		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, status, xhr){
				alert("Uploaded");
			}
		});
	});

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

  1. java.util.UUID 의 정적메서드 randomUUID()로 랜덤 문자열 얻어서 구분자로 합쳐서 파일저장. > 이후 구분자로 잘라내 파일 이름 얻기 위함.
  2. 한폴더 안에 너무 많은 파일이 있지않도록 날짜별로 폴더생성 > SimpleDateFormat 사용 
private String getFolder() {
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

		Date date = new Date();

		String str = sdf.format(date);
		return str.replace("-", File.separator);
	}

File.separator 는 \를 의미함. 즉, 년\월\일 경로.

@PostMapping("uploadAjaxAction")
	public void uploadAjaxPost(MultipartFile[] uploadFile) {

		String uploadFolder = "C:\\upload";

		// make Folder -------
		File uploadPath = new File(uploadFolder, getFolder());// 날짜 폴더 까지 합쳐서 생성.
		log.info("upload Path : " + uploadPath);

		if (uploadPath.exists() == false) {
			uploadPath.mkdirs();
		}
		// make yyyy/MM/dd 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());
			}
		}
	}

여기서 구분자는 _ 


이미지 파일의 구분을 통한 썸네일처리

이미지 파일은 구분자를 붙여 저장된 이미지 외에 작은 썸네일 이미지를 만들어 보여준다.

모바일 환경에서의 데이터 소비를 줄여주기 때문에, 반드시 썸네일은 제작하게 된다.

 

대부분 별도의 라이브러리를 이용한다. Thumbnailator를 사용해본다.

<!-- thumbnailator -->
		<dependency>
		    <groupId>net.coobird</groupId>
		    <artifactId>thumbnailator</artifactId>
		    <version>0.4.8</version>
		</dependency>

 

이후 이미지 파일의 판단은 직접 메서드를 만들어 확인한다.

Files의 정적메서드 probeContentType(Path) 로 MIMETYPE을 리턴받아 그값이  image 로 시작하는지를 파악한다.

private boolean checkImageType(File file) {
		try {
			String contentType = Files.probeContentType(file.toPath());
			return contentType.startsWith("image");
		} catch (Exception e) {
			e.printStackTrace();
		}
		return false;
	}

Thumbnailator의 정적메서드 createThumbnail( InputStream, OutputStream , width, height) 를 이용하여 썸네일을 만든다.

inputStream은 MultipartFile객체의 getInputStream() 메서드로 가져오고 FileOutputStream 은 저장한 이미지 이름에 s_를 붙여 구분자를 넣은 File객체를 매개값으로 지정한다. 

@PostMapping("/uploadAjaxAction")
	public void uploadAjaxPost(MultipartFile[] uploadFile) {

		String uploadFolder = "C:\\upload";

		// make Folder -------
		File uploadPath = new File(uploadFolder, getFolder());// 날짜 폴더 까지 합쳐서 생성.
		log.info("upload Path : " + uploadPath);

		if (uploadPath.exists() == false) {
			uploadPath.mkdirs();
		}
		// make yyyy/MM/dd 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);

			UUID uuid = UUID.randomUUID();

			uploadFileName = uuid.toString() + "_" + uploadFileName;

			log.info("only file Name : " + uploadFileName);

			File saveFile = new File(uploadPath, uploadFileName);

			try {
				multipartFile.transferTo(saveFile);
			} catch (Exception e) {
				log.error(e.getMessage());
			}
			// image File Check
			if (checkImageType(saveFile)) {
				try {
					FileOutputStream fos = new FileOutputStream(new File(uploadPath, "s_" + uploadFileName));

					Thumbnailator.createThumbnail(multipartFile.getInputStream(), fos, 100, 100);
					fos.close();
                } catch (Exception e) {
					e.printStackTrace();
				}
			}

		}
	}

업로드된 데이터의 반환(@ResponseBody)

브라우저에서 피드백을 받을 수 있게끔 필요한데이터를 보내주어야한다.

  1. 업로드된 파일이름 
  2. 원본파일이름
  3. 저장경로
  4. 이미지의 여부

파일의 저장 > 클래스의 List에 add > 이후 ResponseEntity의 body에 넘겨준다. 

 

<!-- jackson -->
	<dependency>
		<groupId>com.fasterxml.jackson.core</groupId>
		<artifactId>jackson-databind</artifactId>
		<version>2.9.5</version>
	</dependency>
	<dependency>
		<groupId>com.fasterxml.jackson.dataformat</groupId>
		<artifactId>jackson-dataformat-xml</artifactId>
		<version>2.9.5</version>
	</dependency>

넘겨줄 클래스

import lombok.Data;

@Data
public class AttachFileDTO {
	private String fileName;
	private String uploadPath;
	private String uuid;
	private boolean image;
}

 확인 부분 : @ResponseBody ResponseEntity<List<AttachDTO>> , produces 속성의 APPLICATION_JSON_VALUE설정

 

                  List<AttatchDTO>의 생성.

                  for의 시작부분에서 AttatchDTO 생성 , 

                  getFolder 메서드(따로 만든..메서드) 의 저장경로값 변수화

                  파일 저장이 완료 된 뒤에 uuid 저장경로를  setter 수집

                  이미지 여부를 확인하는 if 문 통과시 setter로 이미지여부 true

                  for의 종료 statement 부분에 list에 add

 

                  ResponseEntity의 body에 list 저장 후 ok 상태코드 저장, 그 후 리턴

@PostMapping(value = "/uploadAjaxAction", produces = { MediaType.APPLICATION_JSON_VALUE })
	@ResponseBody
	public ResponseEntity<List<AttachFileDTO>> uploadAjaxPost(MultipartFile[] uploadFile) {
		List<AttachFileDTO> list = new ArrayList<>();
		String uploadFolder = "C:\\upload";

		String uploadFolderPath = getFolder();

		// make Folder -------
		File uploadPath = new File(uploadFolder, uploadFolderPath);// 날짜 폴더 까지 합쳐서 생성.
		log.info("upload Path : " + uploadPath);

		if (uploadPath.exists() == false) {
			uploadPath.mkdirs();
		}
		// make yyyy/MM/dd folder

		for (MultipartFile multipartFile : uploadFile) {
			AttachFileDTO dto = new AttachFileDTO();
			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);
			dto.setFileName(uploadFileName);

			UUID uuid = UUID.randomUUID();

			uploadFileName = uuid.toString() + "_" + uploadFileName;

			log.info("only file Name : " + uploadFileName);

			File saveFile = new File(uploadPath, uploadFileName);

			try {
				multipartFile.transferTo(saveFile);
				dto.setUuid(uuid.toString());
				dto.setUploadPath(uploadFolderPath);

				// image File Check
				if (checkImageType(saveFile)) {

					dto.setImage(true);
					FileOutputStream fos = new FileOutputStream(new File(uploadPath, "s_" + uploadFileName));

					Thumbnailator.createThumbnail(multipartFile.getInputStream(), fos, 100, 100);
					fos.close();
				}
				list.add(dto);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return new ResponseEntity<List<AttachFileDTO>>(list, HttpStatus.OK);
	}

ajax 호출에서 dataType을 json으로 선언.

$.ajax({
			url : "/uploadAjaxAction",
			processData: false,
			contentType:false,
			data:formData,
			type:"post",
			dataType:"json",
			success:function(result, status, xhr){
				console.log(result);
			}

 

이후 콜백을 console로 찍으면 Object의 배열이 넘어오는 것을 확인할 수 있다.