[Spring][쇼핑몰 프로젝트] 24. 상품 이미지 업로드(2)

YB·2023년 2월 26일
0

쇼핑몰

목록 보기
36/40
post-thumbnail

목표

서버에서 업로드(컴퓨터에 저장) 처리하기 위해서는 기본적으로 사용자가 뷰(view)에서 업로드할 이미지를 선택하고, 선택된 이미지 파일을 서버로 전송을 해야 합니다. 사용자가 이미지를 선택할 수 있도록 UI를 추가하고 선택된 파일이 개발자가 허용하는 파일인지 체크하는 것을 목표로 합니다.

1. 업로드 UI 추가

사용자가 이미지를 추가할 수 있는 인터페이스를 추가해주겠습니다. 'goodsEnroll.jsp'페이지에서 상품 정보 작성 마지막 항목 공간에 아래의 새로운 항목 공간을 추가해줍니다.

  • class속성명 "form_section_content"인 <div>태그 안에 type이 'file'인 <input>태그를 추가해줍니다. 만약 파일 여러개 선택을 허용하고 싶다면 'multiple'속성을 부여해주면 됩니다. (해당 input태그에만 높이를 주기 위해서 style속성을 부여하였습니다.)

                    		<div class="form_section">
                    			<div class="form_section_title">
                    				<label>상품 이미지</label>
                    			</div>
                    			<div class="form_section_content">
									<input type="file" id ="fileItem" name='uploadFile' style="height: 30px;" multiple>
                    			</div>
                    		</div> 

2. 선택한 파일 접근하기

정리
우리는 사용자가 선택한 파일을 서버에 전송을 하기 위해서 선택된 파일에 접근하는 방법을 알아야 합니다. 먼저 파일 정보가 어떠한 형태로 보관이 되는지 살펴봅니다.

<input>태그를 통해 사용자에 의해 선택된 파일을 File객체의 형태로 표현됩니다. 이 File객체는 FileList객체의 요소로 저장이 됩니다.

FileList는 배열 형태의 객체입니다. FileList의 요소에는 File객체가 저장됩니다. 이 File객체는 type이 'file'인 <input>태그의 "files"속성입니다.

Files와 FileList객체의 관계를 단순히 그림의 형태로 표현하면 다음과 같습니다.

사용자가 <input>태그를 통해서 파일 1개를 선택을 하게 된다면 FileList 첫번째 요소(FileList[0])인 File객체에 파일 데이터가 저장되게 됩니다. 반대로 사용자가 여러개의 파일을 선택을 한다면 선택한 객수(n)만큼 FileList 첫번째 요소(FileList[0])부터 순서대로 각 요소(FileList[n]) FIle객체에 저장되게 됩니다.

그렇다면 사용자가 선택한 파일을 File객체에 접근하기 위해선 결국 FileList 객체(<input>태그의 files속성)에 접근을 해야합니다. 해당 FileList객체의 접근은 <input>태그의 change이벤트를 통해서 접근이 가능하게 됩니다.

type이 file인 <input>태그에 접근하기 위해서 먼저 해당 <input>태그가 change이벤트가 일어났을 때 동작하는 메서드를 <script>태그에 추가합니다. 추가한 메서드가 동작하는지를 확인하기 위해서 alert()메서드를 추가하였습니다.

/* 이미지 업로드 */
$("input[type='file']").on("change", function(e){
	alert("동작");
});

  • leList객체가 맞는지 확인하기 위해 변수를 선언하고 fileList로 초기화합니다.
  • Filelist의 요소로 있는 File객체에 접근해보겠습니다. FileList객체의 데이터 타입이 배열이기 때문에 일반적으로 요소에 접근하는 방식인 인덱스를 사용하여 File객체에 접근합니다.
  • File객체에 담긴 데이터가 <input>태그를 통해 선택한 파일의 데이터가 맞는지를 확인해보겠습니다. File인터페이스가 가진 속성을 사용하여 파일 이름, 파일 사이즈, 파일 타입을 console.log를 통해 출력시켜보겠습니다.
	let fileInput = $('input[name="uploadFile"]');
	let fileList = fileInput[0].files;
	let fileObj = fileList[0];
	
	console.log("fileList : " + fileList);
	console.log("fileObj : " + fileObj);
	console.log("fileName : " + fileObj.name);
	console.log("fileSize : " + fileObj.size);
	console.log("fileType(MimeType) : " + fileObj.type);

3. 파일 체크(Javscript)

이번에는 뷰(View) 단계에서 사용자가 선택 한 파일이 개발자가 허용하는 파일이 아닐 시에 경고창과 함께 <input> change이벤트 메서드에서 벗어나도록 구현해보겠습니다.

이미지 파일인 jpg, png만 허용으로 하고 파일의 크기는 1MB의 크기만 허용할 것입니다. 그리고 기존의 테스트 console.log는 지우고 if문을 작성하여 if문의 조건문에는 fileCheck()메서드와 not논리 연산자(!)를 if문의 구현부에는 return false를 작성합니다. 그렇게 alert을 작성하여 경고창이 뜨도록 합니다.

		if(!fileCheck(fileObj.name, fileObj.size)){
			return false;
		}
		
		alert("통과");

/* var, method related with attachFile */
let regex = new RegExp("(.*?)\.(jpg|png)$");
let maxSize = 1048576; //1MB	

function fileCheck(fileName, fileSize){

	if(fileSize >= maxSize){
		alert("파일 사이즈 초과");
		return false;
	}
		  
	if(!regex.test(fileName)){
		alert("해당 종류의 파일은 업로드할 수 없습니다.");
		return false;
	}
	
	return true;		
	
}



4. 업로드 UI 추가

첨부 파일을 서버로 전송하기 위해선 FormData객체를 사용해야 합니다. 우리는 흔히 뷰(View)에 서버로 데이터를 전송하기 위해서 <form>태그를 사용하였습니다. 화면의 이동을 하면서 서버로 첨부파일 전송을 하는 것이라면 <form>태그를 사용하면 됩니다. 하지만 우리는 화면의 이동 없이 첨부파일을 서버로 전송을 해야 합니다. 따라서 <form>태그와 같은 역할을 해주는 FormData객체를 생성하여 첨부파일을 FormData에 저장을 하고 FormData자체를 서버로 전송해야 합니다.

<input> change메서드 구현부에 FormData객체를 인스턴스화 하여 변수에 저장합니다.

FormData객체에 데이터를 추가하는 방법은 FormData.append(key, value)메서드를 사용하는 것입니다. 파라미터 key의 경우 추가해줄 데이터의 변수를 작성한다고 생각하면 됩니다. <input> name과 Controller의 url매핑 메서드의 매개변수 이름과 동일하게 해주었듯이, key와 추후 추가할 url매핑 메서드의 매개변수명이 동일해야 합니다.

기존 특정 Key가 있는 상태에서 동일한 Key로 데이터를 추가하면 기존 값을 덮어쓰지 않고 기존 값 집합의 끝에 새로운 값을 추가합니다.(서버에서는 배열 타입으로 데이터를 전달받습니다.)

사용자가 선택한 파일을 FormData에 "uploadFile"이란 이름(key)으로 추가해주는 코드를 작성합니다. (multiple이 없는 경우 for문만 지워줍니다.)

	let formData = new FormData();
    
    for(let i = 0; i < fileList.length; i++){
		formData.append("uploadFile", fileList[i]);
	}

5. 첨부파일 서버 전송

준비된 데이터를 서버에 전송하는 코드를 작성할 것입니다. AJAX를 사용하여 서버로 전송하는 아래의 코드를 추가해줍니다.

ajax코드에서 한가지 주의할 점은 processData와 contenttype속성의 값을 'false'로 해주어야만 첨부파일이 서버로 전송된다는 점입니다.

ajax에 작성한 각 속성 내용입니다.

  • url : 서버로 요청을 보낼 url
  • processData : 서버로 전송할 데이터를 queryStirng 형태로 변환할지 여부
  • contentType : 서버로 전송되는 데이터의 content-type
  • data : 서버로 전송할 데이터
  • type : 서보 요청 타입(GET, POST)
  • dataType : 서버로부터 반환받을 데이터 타입
	$.ajax({
		url: '/admin/uploadAjaxAction',
    	processData : false,
    	contentType : false,
    	data : formData,
    	type : 'POST',
    	dataType : 'json'
	});

6. URL 매핑 메서드

뷰(View)에서의 요청에 응답할 url매핑 메서드를 추가하겠습니다. AdminController.java에 "/admin/uploadajaxAction"url요청에 응답을 하는 url매핑 메서드를 작성합니다. (뷰로 결과를 반환해야 하기 때문에 반환 타입을 특정 타입으로 지정을 해주어야 하지만, 임의로 void로 지정을 하였습니다.

뷰(view)에서 전송한 첨부파일 데이터를 여러개 전달받기 위해서 MultipartFile타입의 "uploadFile"변수를 배열 타입의 매개변수로 부여합니다. MultipartFile배열 타입의 uploadFile의 모든 요소의 데이터 정보를 출력시키고 싶다면 for문을 활용하면 됩니다.

	/* 첨부 파일 업로드 */
	@PostMapping("/uploadAjaxAction")
	public void uploadAjaxActionPOST(MultipartFile[] uploadFile) {
		
		logger.info("uploadAjaxActionPOST..........");
		String uploadFolder = "D:\\upload";
		
		// 향상된 for
		for(MultipartFile multipartFile : uploadFile) {
			
		}
		
	}

업로드 될 파일을 "d/upload"경로에 저장할 것입니다. d경로에 upload폴더를 만들어주고 우리가 설정한 용량을 초과하는 파일들을 d/upload/temp폴더에 저장되도록 설정하는 코드를 추가합니다. 따라서 temp폴더 또한 만들어줍니다.

7. 날짜 경로 문자열 얻기

업로드 될 파일들을 단순히 "d/upload"폴더에 저장시켜도 상관은 없지만 매우 많은 파일을 한 곳에 업로드를 할 경우 앞으로 구현할 업로드된 이미지를 호출할 때 애플리케이션 내에서 호출한 파일을 찾기 위해 upload폴더 전체를 스캔을 하게 되는데 그 과정에서 자원이 많이 소모될 수 있습니다. 더불어 파일을 관리하는 측면에서도 너무 많은 파일이 한 곳에 모여있으면 어려움이 있습니다.

이러한 문제점을 보완하기 위해서 업로드 하는 날짜에 맞게 폴더가 생성되고, 생성된 폴어데 업로드 파일을 저장되도록할 것입니다. 예를 들어 '2023년 2월 26일'날짜의 경우 d/upload 경로에 '2023/02/26' 경로의 폴더가 생성되도록 할 것입니다.

Java에서 제공하는 File이라는 클래스를 통해서 폴더를 생성해 줄것입니다. 하지만 폴더를 생성하는 명령을 수행하기 위해선 생성할 폴더의 전체 경로의 String데이터가 필요로 합니다. 일단 기본적인 경로인 "d:\upload"는 변수로 선언이 되어 있기 때문에 하위 경로인 "yyyy/MM/dd"문자열(String)데이터가 필요로 합니다. 따라서 오늘 날짜의 "yyyy/MM/dd"문자열 데이터를 생성하도록 코드를 작성해보겠습니다.

오늘 날짜의 'yyyy/MM/dd'형식의 String데이터를 얻기 위해서 SimpleDateFormat클래스와 Date클래스를 사용할 것입니다. Date클래스를 오늘의 날짜를 얻기 위해서 사용을 합니다. SimpleDateFormat은 Date클래스를 통해 얻은 오늘의 날짜를 지정된 형식의 문자열 데이터로 생성하기 위해서 사용을 합니다. SimpleDateFormat에 대해서 간략히 설명을 하면 날짜 데이터를 지정된 문자열 형식으로 변환하거나 날짜 문자열 데이터를 날짜 데이터로 변환할 수 있게 해주는 클래스입니다.

  • SimpleDateFormat을 사용하기 위해서 아래와 같이 변수를 선언하고 인스턴스화하여 초기화해줍니다. 인스턴스화하는 코드의 파라미터에는 자신이 날짜데이터를 어떠한 형식의 문자열로 변환할 지 지정해주는 값을 삽입합니다.
  • 오늘의 날짜 데이터를 얻기 위해서 Date클래스 타입의 변수를 선언 및 초기화 해줍니다.
  • 오늘의 날짜 데이터 값을 가지고 있는 date변수를 "yyyy-MM-dd"형식의 문자열로 변환을 해주기 위해서 SimpleDateFormat의 format메서드를 호출합니다. format메서드의 인자 값으로 date변수를 부여합니다. String타입으로 변환된 값을 String타입의 str변수를 선언하여 대입합니다.
  • str의 데이터는 년, 월, 일 사이에 '-'이 작성되어 있습니다. '-'을 경로 구분자인 '/'혹은 '\'로 변경해주어야 합니다. str데이터에서 '-'값만 변경해주기 위해서 String클래스가 가지고 있는 replace메서드를 사용할 것입니다.
		/* 날짜 폴더 경로 */
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
		
		Date date = new Date();
		
		String str = sdf.format(date);
		
		String datePath = str.replace("-", File.separator);

8. 폴더 생성 (File 객체 사용)

폴더를 생성하기 위해서 File클래스를 사용을 할 것인데, File클래스의 역할에 대해 간략히 설명을 하면 Java에서 '파일' 혹은 '디렉터리'에 관한 작업을 할 수 있도록 여러 메서드와 변수를 제공해주는 클래스입니다.

  • File타입의 uploadpath변수를 선언하여 우리가 만들고자 하는 "c:\upload\yyyy\MM\dd'경로의 디렉터리를 대상으로 하는 File객체로 초기화해줍니다. 객체화해주는 코드에 첫 번째 인자로 부모 경로엔 uploadFolder변수를 두 번째인자로 자식 경로인 datePath변수를 부여해줍니다.
  • 폴더 생성을 수행하기 위해서 File클래스의 mkdir()혹은 mkdirs()를 사용할 수 있습니다. 두 메서드는 폴더를 생성한다는 것을 동일 하지만 한 개의 폴더를 생성할 수 있느냐 여러 개의 폴더를 생성할 수 있느냐의 차이가 있습니다. 우리는 여러개의 폴더를 생성해야 하기 때문에 mkdirs()메서드를 사용합니다.
  • 폴더가 이미 존재하는 상황에도 사용자가 업르드를 할 때마다 폴더를 생성하는 걸 방지하기 위해서 File클래스에서 대상 파일 혹은 디렉터리가 존재하는지 유무를 반환하는 exists()메서드를 활용하여 if문을 작성을 하고 if구현부에 폴더를 생성하는 코드를 작성해줍니다.
		/* 폴더 생성 */
		File uploadPath = new File(uploadFolder, datePath);
		
		if(uploadPath.exists() == false) {
			uploadPath.mkdirs();
		}

9. 파일 저장

뷰로부터 전달 받은 파일을 지정한 폴더에 저장하기 위해서 MultipartFile클래스의 transferTo()메서드를 사용할 것입니다. 사용방법은 전달받은 파일인 MultipartFile객체에 저장하고자 하는 위치를 지정한 File객체를 파라미터로 하여 transferTo()메서드를 호출하면 됩니다.

따라서 tansferTo()메서드를 사용하기 위해서 저장될 파일 이름과 위치 전체를 포함하는 File객체를 만들어주겠습니다. 파일 이름의 경우 뷰로부터 전달받은 파일 이름을 그대로 사용할 것이기 때문에 getOriginalFilename()메서드를 사용할 것입니다.

File객체를 만들어 주기 전 먼저 파일의 이름을 사용하기 위해서 String타입의 uploadfileName변수를 선언하여 파일 이름을 저장해줍니다. 파일 저장 위치인 uploadPath와 파일 이름인 uploadFileName을 활용하여 File타입의 saveFile변수를 선언하고 파일 경로와 파일 이름을 포함하는 File객체로 초기화해줍니다.

파일을 저장하는 메서드인 transferTo() 호출합니다. transferTo()의 경우 IOException와 illegalStateException을 일으킬 가능성이 있기 때문에 컴파일러에서 try catch문을 사용하라는 경구문이 뜹니다. 따라서 파일을 저장하는 코드를 try catch문으로 감싸줍니다.

			/* 파일 이름 */
			String uploadFileName = multipartFile.getOriginalFilename();			
			
			/* 파일 위치, 파일 이름을 합친 File 객체 */
			File saveFile = new File(uploadPath, uploadFileName);
			
			/* 파일 저장 */
			try {
				multipartFile.transferTo(saveFile);
			} catch (Exception e) {
				e.printStackTrace();
			} 

10. UUID 적용

한가지 문제가 있습니다. 동일한 이름을 가진 파일을 저장하게 되면 기존의 파일을 덮어씌워 버린다는 점입니다. 이러한 문제점을 보완하기 위해서 각 파일이 저장될 때 고유한 이름을 가지도록 하면 해결이 될 것입니다. 따라서 파일에 고유한 이름을 가지도록 하기 위해서 기존 파일 이름에 UUID가 포함되도록 할 것입니다.

UUID(범용 고유 식별자)는 쉽게 말해 국제기구에서 표준으로 정한 식별자(일련번호)라고 생각하시면 됩니다. UUID는 총 5개 버전이 있으며 각 버전에 따라 식별자 생성 방식이 다릅니다.

이를 Java에서도 UUID라는 클래스를 통해 구제기구에서 표준으로 정한 식별자를 사용할 수 있도록 하고 있습니다. Java에서도 5개의 버전 방식으로 제공을 하고 있는데 그중 가장 쉽게 사용할 수 있는 버전 4(랜덤) 방식의 radomUUID() 메서드를 사용할 것입니다. 해당 메서드는 정적(static) 메서드 이기 때문에 UUID를 인스턴스화 하지 않고도 사용할 수 있습니다. 주의할 점은 UUID.randomUUID()를 통해 생성된 '식별자'는 UUID 타입의 데이터 이기 때문에 toString() 메서들 사용하여 String 타입으로 변경해주어야 우리가 사용할 수 있습니다.

우리는 파일 이름을 "UUID파일 이름"과 같은 방식으로 저장하도록 할 것입니다. 먼저 UUID를 저장할 String타입의 변수 uuid를 선언하고 UUID로 초기화해줍니다. 기존 파일 이름인 uploadFileName 변수를 "UUID파일 이름"형식이 되도록 변경해줍니다.

			/* uuid 적용 파일 이름 */
			String uuid = UUID.randomUUID().toString();
			
			uploadFileName = uuid + "_" + uploadFileName;

profile
개인이 공부한걸 작성하는 블로그입니다..

0개의 댓글