[31] 파일 업로드 처리 - 파일 업로드 상세 처리

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

최근 포털에서도 특정한 확장자를 제외한 파일들의 업로드를 제한하는 경우가 많은데, 이는 첨부파일을 이용하는 웹 공격을 막기 위해 행해지는 조치이다!  이 책의 예제에서는 첨부파일의 확장자가 'exe, sh, zip' 등의 경우에는 업로드를 제한하고, 특정 크기 이상의 파일은 업로드할 수 없도록 제한하는 처리를 JavaScript로 처리한다. 파일 확장자의 경우 정규 표현식을 이용해서 검사할 수 있다.

 

1) uploadAjax.jsp 파일의 확장자와 크기를 설정하고, 이를 검사하는 함수 checkExtension()을 작성해서 적용하면 아래와 같은 형태가 된다. 

<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 file data 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");
			}
		}); // $.ajax
	});
});
</script>

 

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

 

사족이지만 내 에러의 80%는 거의다 오타인것 같다.. ㅡㅡ.. 

 

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

 

첨부파일을 저장할 때 신경 쓰이는 것은 크게 두 가지로 1) 중복된 이름의 파일 처리와 2) 한 폴더 내에 너무 많은 파일의 생성 문제이다. 

 

1) 의 경우는 현재 시간을 밀리세컨드까지 구분해서 파일 이름을 생성해서 저장하거나 UUID를 이용해서 중복이 발생할 가능성이 거의 없는 문자열을 생성해서 처리한다. 

2) 의 경우는 하나의 폴더에 생성될 수 있는 파일의 개수에 대한 문제인데, 한 폴더에 너무 많은 파일이 있는 경우 속도의 저하와 개수의 제한 문제가 생기는 것을 방지해야 한다. 이에 대한 해결책으로 일반적인 방법은 '년/월/일' 단위의 폴더를 생성해서 파일을 저장하는 것이다.

 

1-2. 년/월/일 폴더의 생성

 

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

 

1) UploadController에 추가적인 메서드와 수정을 통해서 업로드 폴더 등을 처리하도록 한다. 

 - 이때 Date는 java.util 로 import 시켜야한다.... 

@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 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);
			
			//File saveFile = new File(uploadFolder, uploadFileName);
			File saveFile = new File(uploadPath, uploadFileName);
			
			try {
				multipartFile.transferTo(saveFile);
			}catch (Exception e) {
				log.error(e.getMessage());
			}//end catch
		}//end for
	}
	
	// 오늘 날짜의 경로를 문자열로 생성한다.
	private String getFolder() {
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
		
		Date date = new Date(0);
		
		String str = sdf.format(date);
		
		return str.replace("-", File.separator);
	}

 

getFolder()는 오늘 날짜의 경로를 문자열로 생성한다. 생성된 경로는 폴더 경로로 수정된 뒤에 반환한다. uploadAjaxPost()에서는 해당 경로가 있는지 검사하고, 폴더를 생성한다. 이후 생성된 폴더로 파일을 저장하게 한다. 위와 같이 폴더를 생성한 후 기존과 달리 uploadPath 경로에 파일을 저장하게 되면 자동으로 폴더가 생성되면서 파일이 저장되는 것을 볼 수 있다. 

 

결과 화면

 

1-3. 중복 방지를 위한 UUID 적용

 

파일 이름을 생성할 때 동일한 이름으로 업로드 되면 기존 파일을 지우게 되므로 java.util.UUID 의 값을 이용해서 처리한다.

 

1) UploadController의 일부에 코드를 추가한다.

 

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

 

이제 첨부파일을 업로드 하면 UUID가 생성된 파일이 생기므로, 원본 이름과 같더라도 다른 이름의 파일로 생성되는 것을 확인할 수 있다. 

댕댕이 한가득

 

2. 섬네일 이미지 생성

 

이미지의 경로에 대한 처리와 중복 이름에 대한 처리가 완료되었다면, 남은 작업은 일반 파일과 이미지 파일을 구분하는 것이다. 이미지 파일의 경우는 화면에 보여지는 작은 이미지를 생성하는 추가적인 처리다. 만일 용량이 큰 파일을 섬네일 처리하지 않는다면 모바일과 같은 환경에서 많은 데이터를 소비해야만 하므로 이미지의 경우는 특별한 경우가 아니라면 섬네일을 제작해야만 한다. 

 

섬네일을 제작한느 방법은 여러가지 방식이 있다. JDK1.4 부터는 ImageIO 를 제공하기 때문에 이를 이용해서 원본 이미지의 크기를 줄일 수도 있고, ImgScalr와 같은 별도의 라이브러리를 이용하는 방식도 있다. JDK에 포함된 API를 이용하는 방식보다는 별도의 라이브러리를 사용하은 경우가 많은데, 이는 이미지를 축소했을 때의 크기나 해상도를 직접 조절하는 작업을 줄이기 위해서이다. 예제에서는 Thumbnailator 라이브러리를 이용해서 섬네일 이미지를 생성한다. 

(https://github.com/coobird/thumbnailator)

 

1) maven 저장소에서 Thumnailator 라이브러리를 검색해서 pom.xml에 추가한다. 

 

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

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

 

2-1. 이미지 파일의 판단

 

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

 

1) 특정한 파일이 이미지 타입인지를 검사하는 별도의 checkImageType() 메서드를 추가한다.

- UploadController.java

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

 

만일 이미지 타입이라면 섬네일을 생성하도록 코드를 수정한다. 

 

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

 

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

 

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

 

첨부파일 데이터의 업로드가 완료되었지만, 아직도 많은 작업이 남아있다. Ajax를 이용해서 파일을 업로드 했지만, 아직 브라우저 쪽에 아무런 데이터도 전달하지 않았기 때문에 브라우저에서는 어떠한 피드백도 받을 수 없는 상황이다. 

 

서버에서 Ajax의 결과로 전달해야 하는 데이터는 업로드된 파일의 경로가 포함된 파일의 이름이다. 섬네일의 경우에는 's_'로 시작한다는 규칙만 알고 있으면 필요할 때 사용할 수 있다.

 

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

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

이에 대한 모든 정보를 처리하는 방법은 1) 업로드된 경로가 포함된 파일 이름을 반환하는 방식과 2) 별도의 객체를 생성해서 처리하는 방법을 고려할 수 있다. 1) 의 경우에는 브라우저 쪽에서 해야 하는 일이 많기 때문에 예제는 2) 의 방식으로 구성하도록 한다. 

 

1) pom.xml에 jackson-databind 관련 라이브러리를 포함한다.

3-1. AttachFileDTO 클래스

 

1) org.zerock.domain 패키지에 첨부파일의 정보들을 저장하는 AttachFileDTO 클래스를 작성한다. 

package org.zerock.domain;

import lombok.Data;

@Data
public class AttachFileDTO {

	private String fileName;
	private String uploadPath;
	private String uuid;
	private boolean image;
}

 

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

 

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

 

2) UploadController.java 일부를 변경한다.

@PostMapping(value = "/uploadAjaxAction", produces = MediaType.APPLICATION_JSON_UTF8_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);

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

		for (MultipartFile multipartFile : uploadFile) {

			AttachFileDTO attachDTO = new AttachFileDTO();

			String uploadFileName = multipartFile.getOriginalFilename();

			// IE has file path
			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);

				// check image type file
				if (checkImageType(saveFile)) {

					attachDTO.setImage(true);

					FileOutputStream thumbnail = new FileOutputStream(new File(uploadPath, "s_" + uploadFileName));

					Thumbnailator.createThumbnail(multipartFile.getInputStream(), thumbnail, 100, 100);

					thumbnail.close();
				}

				// add to List
				list.add(attachDTO);

			} catch (Exception e) {
				e.printStackTrace();
			}

		} // end for
		return new ResponseEntity<>(list, HttpStatus.OK);
	}

 

uploadAjaxPost()는 기존과 달리 ResponseEntity<List<AttachFileDTO>>를 반환하는 형태로 수정하고, JSON 데이터를 반환하도록 변경된다. 내부에서는 각 파일에 맞게 AttachFileDTO를 생성해서 전달하는 구조로 변경된다.

 

3-2. 브라우저에서 Ajax 처리

 

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

Ajax를 호출했을 때의 결과 타입은 'json'으로 변경하고, 결과를 console.log()로 찍도록 변경한다. 첨부파일을 업로드한 후에는 브라우저에서 결과를 아래와 같이 확인할 수 있다.