개발/[Spring] 블로그 만들기

[코드로 배우는 스프링 웹 프로젝트] 22강. 파일 업로드 상세 처리하기 (Ajax, Thumbnailator, 섬네일 생성하기)

ee2ee2 2022. 6. 8. 22:36
728x90
반응형

해당 프로젝트는 코드로 배우는 스프링 웹 프로젝트(개정판)을 기반으로 진행됩니다.


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

첨부파일을 이용하는 웹 공격을 막기 위해 특정 확장자의 파일 업로드를 제한할 필요가 있다. 예제는 첨부파일의 확장자가 'wxw, sh, zip'등의 경우레는 업로드를 제한하고, 특정 크기 이상의 파일은 업로드할 수 없도록 제한하는 처리를 JavaScript로 처리한다.

 

uploadAjax.jsp의 일부

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

실행 결과

exe 확장자의 파일 업로드가 제한됨을 확인할 수 있다.


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

첨부파일 저장시 고려할 점으로 1) 중복된 이름의 파일 처리, 2) 한 폴더 내에 너무 많은 파일의 저장 2가지가 있다.

1)의 경우에는 현재 시간을 ms까지 구분해서 파일 이름을 생성하여 저장하거나, UUID를 이용해서 중복 발생가능성이 거의 없는 문자열을 통해 처리할 수 있다. 2)의 경우에는 하나의 폴더에 너무 많은 파일이 있는 경우에는 속도 저하가 발생할 수 있다. 이에 대한 해결책으로 '년/월/일'단위의 폴더를 생성하여 파일을 저장하는 방법이 있다.


22.1.2 년/월/일 폴더의 생성

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

UploadController.java 컨트롤러 일부

	//오늘 날짜의 경로를 문자열로 생성하는 함수
	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 uploadAjaxAction(MultipartFile[] uploadFile) {
		
		log.info("Update Ajax POST...............");
		
		String uploadFolder = "C:\\upload";
		
		// make folder ----------
		File uploadPath = new File(uploadFolder, getFolder());	//uploadFolder 밑에 getFolder로 얻은 경로로 File 객체 생성
		log.info("upload Path : " + uploadPath);
		
		
		boolean isuploadPath = uploadPath.exists();
		if(isuploadPath == 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(uploadFolder, uploadFileName);
			File saveFile = new File(uploadPath, uploadFileName);
			
			try {
				multipartFile.transferTo(saveFile);
			} catch (Exception e) {
				// TODO Auto-generated catch block
				log.error(e.getMessage());
			}
		}
	}

실행 결과

오늘 날짜(년/월/일)의 폴더에 업로드 파일명 앞에 UUID가 붙여 파일명이 저장됨.


22.2 섬네일 이미지 생성

앞서 이미지의 경로에 대한 처리와 중복 이름에 대한 처리가 완료되었으니, 이제 일반 파일과 이미지 파일을 구분해야 한다. 이미지 파일의 경우 화면에 보여지는 썸네일 이미지를 생성하는 추가 작업을 통해 용량을 줄여 많은 데이터 소비를 막을 수 있다. (모바일 환경에서)

Thumbnailator 라이브러리를 이용하여 섬네일 이미지를 생성해보자.

https://github.com/coobird/thumbnailator

 

GitHub - coobird/thumbnailator: Thumbnailator - a thumbnail generation library for Java

Thumbnailator - a thumbnail generation library for Java - GitHub - coobird/thumbnailator: Thumbnailator - a thumbnail generation library for Java

github.com

pom.xml에 추가

..생략

<!-- 섬네일 이미지 만들기 라이브러리 추가 : https://mvnrepository.com/artifact/net.coobird/thumbnailator-->
<dependency>
	<groupId>net.coobird</groupId>
	<artifactId>thumbnailator</artifactId>
	<version>0.4.8</version>
</dependency>

..생략

 

UploadController에서는 아래의 2단계를 이용해서 섬네일을 생성한다.

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

22.2.1 이미지 파일의 판단

Files.probeContentType()을통해 파일의 MIME Type을 Return 받고, image 텍스트를 포함하는지 판별한다.

<MIME Type 예>

text/plain
text/html
image/jpeg
image/png
audio/mpeg
audio/ogg audio/*
video/mp4
application/octet-stream …

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;
	}

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

UploadController.java 컨트롤러 일부

/uploadAjaxAction을 이용해서 이미지 파일을 업로드하면 원본 파일은 그대로 저장되고, 파일 이름이 's_'로 시작하는 섬네일 파일이 생성된다.

	@PostMapping(value="/uploadAjaxAction")
	public void uploadAjaxPost(MultipartFile[] uploadFile) {
		log.info("Update Ajax POST...............");
		
		String uploadFolder = "C:\\upload";
		String uploadFolderPath = getFolder();
		
		// make folder ----------
		File uploadPath = new File(uploadFolder, uploadFolderPath);	//uploadFolder 밑에 getFolder로 얻은 경로로 File 객체 생성
		log.info("upload Path : " + uploadPath);
		
		boolean isuploadPath = uploadPath.exists();
		if(isuploadPath == 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();			
			String uuid_uploadFileName = uuid.toString() + "_" + uploadFileName;
			
			try {
				File saveFile = new File(uploadPath, uuid_uploadFileName);
				multipartFile.transferTo(saveFile);
				
				//check image type file
				if(checkImageType(saveFile)) {			
					FileOutputStream thumbnail = new FileOutputStream(new File(uploadPath, "s_" + uuid_uploadFileName));
					
					Thumbnailator.createThumbnail(multipartFile.getInputStream(), thumbnail, 100, 100);
					thumbnail.close();
				}				
			} catch (Exception e) {
				log.error(e.getMessage());
			}
		}
	}

실행 결과

이미지의 경우, 섬네일이 생성됨
이미지가 아닌 경우, 섬네일 생성 안됨

 


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

Ajax를 이용하여 파일을 업로드했지만, 아직 브라우저 쪽에 아무런 데이터도 전달하지 않았기 때문에 브라우저에서는 어떠한 피드백도 받을 수 없다. 서버에서 Ajax의 결과를 전달해야 하는 데이터는 업로드된 파일의 경로가 포함된 파일의 이름이다. 섬네일의 경우 '_s'로 시작한다는 규칙만 알면 된다!

<브라우저로 전송해야하는 데이터>

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

위 데이터를 객체를 생성하여 처리해보자.

pom.xml의 일부

		<!-- jackson-databind : 브라우저에 객체를 JSON/XML 포맷의 문자열로 변환시켜 전송할 때 필요 -->    
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
			<version>2.9.6</version>
		</dependency>
		
		<dependency>
			<groupId>com.fasterxml.jackson.dataformat</groupId>
			<artifactId>jackson-dataformat-xml</artifactId>
			<version>2.9.6</version>
		</dependency>

22.3.1 AttachFileDTO 클래스

AttachFileDTO 클래스

AttachFileDRO 클래스는 원본파일의 이름(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;	
}

 

UploadController 컨트롤러 일부

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

	@PostMapping(value="/uploadAjaxAction", produces=MediaType.APPLICATION_JSON_UTF8_VALUE)
	@ResponseBody
	public ResponseEntity<List<AttachFileDTO>> uploadAjaxAction(MultipartFile[] uploadFile) {
		log.info("Update Ajax POST...............");
			
		List<AttachFileDTO> list = new ArrayList<>();
		
		String uploadFolder = "C:\\upload";
		String uploadFolderPath = getFolder();
		
		// make folder ----------
		File uploadPath = new File(uploadFolder, uploadFolderPath);	//uploadFolder 밑에 getFolder로 얻은 경로로 File 객체 생성
		log.info("upload Path : " + uploadPath);
		
		boolean isuploadPath = uploadPath.exists();
		if(isuploadPath == false) {
			uploadPath.mkdirs(); 	//make yyyy/MM/dd folder
		}
		
		for(MultipartFile multipartFile : uploadFile) {
			
			AttachFileDTO attachDTO = 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);			
			log.info("Only file name : " + uploadFileName);
	
			UUID uuid = UUID.randomUUID();			
			String uuid_uploadFileName = uuid.toString() + "_" + uploadFileName;
			
			attachDTO.setFileName(uploadFileName);
			attachDTO.setUuid(uuid.toString());
			attachDTO.setUploadPath(uploadFolderPath);
			
			//File saveFile = new File(uploadFolder, uploadFileName);
			//File saveFile = new File(uploadPath, uploadFileName);
			
			try {
				File saveFile = new File(uploadPath, uuid_uploadFileName);
				multipartFile.transferTo(saveFile);
				
				//check image type file
				if(checkImageType(saveFile)) {
					attachDTO.setImage(true);
					
					FileOutputStream thumbnail = new FileOutputStream(new File(uploadPath, "s_" + uuid_uploadFileName));
					
					Thumbnailator.createThumbnail(multipartFile.getInputStream(), thumbnail, 100, 100);
					thumbnail.close();
				}
				
				//add to list
				list.add(attachDTO);
				
			} catch (Exception e) {
				log.error(e.getMessage());
			}
		}
		
		return new ResponseEntity<>(list, HttpStatus.OK);
	}

 

22.3.2 브라우저에서 Ajax 처리

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

uploadAjax.jsp

..생략
		$.ajax({
				url: '/uploadAjaxAction',
				processData: false,
				contentType: false,
				data: formData,
					type: 'POST',
					dataType : 'json', // --> 수정 부분
					success: function(result){
						console.log(result);
				}
			}); //$.ajax

실행 결과


다음은 브라우저에서 섬네일 처리 방법에 대해 구현해볼 것 이다.