관리 메뉴

Just Do it

Part 6) 파일 업로드 처리 (Ch 24) 본문

카테고리 없음

Part 6) 파일 업로드 처리 (Ch 24)

Seojoo21 2022. 3. 10. 00:42

* 이 카테고리의 글은 <코드로 배우는 스프링  프로젝트에서  파트별로 진행하는 프로젝트의 흐름을 보기 위해  장의 내용을 간단히 요약한 것이다.

출처코드로 배우는 스프링  프로젝트 개정판구멍가게 코딩단남가람북스 

 

Ch24 첨부파일의 다운로드 혹은 원본 보여주기

- 첨부파일의 업로드가 처리되는 과정도 복잡하지만, 이를 사용자가 사용하는 과정 역시 신경 써야 하는 일이 많다.

- 브라우저에서 보이는 첨부파일은 크게 1) 이미지 종류와 2) 일반 파일로 구분되므로 사용자의 첨부파일과 관련된 행위도 종류에 따라 다르게 처리되어야 한다. 

 

- 만일 첨부파일이 이미지인 경우에는 섬네일 이미지를 클릭했을 때 화면에 크게 원본 파일을 보여주는 형태로 처리되어야 한다. - 이 경우는 브라우저에서 새로운 <div> 등을 생성해서 처리하는 방식을 이용하는데 흔히 'light-box' 라고 한다. - 'light-box'는 jQuery를 이용하는 많은 플러그인들이 있으므로, 이를 이용하거나 직접 구현할 수 있다. 예제는 직접 구현하는 방식으로 한다.

 

- 첨부파일이 이미지가 아닌 경우에는 기본은 다운로드이다. - 사용자가 파일을 선택하면 다운로드가 실행되면서 해당 파일의 이름으로 다운로드가 가능해야 한다. (여기에 한글 이름 처리 등이 이슈가 될 수 있다.) 

 

24.1 첨부파일의 다운로드 

- 이미지를 처리하기 전에 우선 좀 더 간단한 첨부파일의 다운로드부터 처리하도록 한다.

- 첨부파일의 다운로드는 서버에서 MIME 타입을 다운로드 타입으로 지정하고, 적절한 헤더 메시지를 통해서 다운로드 이름을 지정하게 처리한다. 

- 이미지와 달리 다운로드는 MIME 타입이 고정되기 때문에 UploadController 내 일반파일 다운로드 메서드는 아래와 같이 시작하게 된다.

// 첨부파일을 다운로드한다. (이미지가 아닌 일반 파일)
	@GetMapping(value = "/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
	@ResponseBody
	public ResponseEntity<Resource> downloadFile(String fileName){
		log.info("download file:" + fileName);
		Resource resource = new FileSystemResource("업로드 파일 경로 " + fileName);
		log.info("resource: " + resource);
		return null;
	}

- ResponseEntity<>의 타입은 byte[] 등을 사용할 수 있으나, 이번 예제에서는 org.springframework.core.io.Resource 타입을 이용해서 좀 더 간단히 처리하도록 한다.

 

- 테스트를 위해서 파일이 업로드 되는 폴더에 영문 파일을 하나 두고, '/download?fileName=파일이름'의 형태로 호출해 본다. 

- 브라우저에는 아무런 반응이 없지만, 서버에는 로그가 기록되는 것을 먼저 확인한다.

- 서버에서 파일이 정상적으로 인식되었다는 것이 확인되면 ResponseEntity<>를 처리한다. 이때 HttpHeaders 객체를 이용해서 다운로드 시 파일의 이름을 처리하도록 한다.

	// 첨부파일을 다운로드한다. (이미지가 아닌 일반 파일)
	@GetMapping(value = "/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
	@ResponseBody
	public ResponseEntity<Resource> downloadFile(String fileName){
		log.info("download file:" + fileName);
		Resource resource = new FileSystemResource("파일 업로드 경로" + fileName);
		log.info("resource: " + resource);
		
		String resourceName = resource.getFilename();
		
		HttpHeaders headers = new HttpHeaders();
		
		try {
			headers.add("Content-Disposition", "attachment; filename=" + new String(resourceName.getBytes("UTF-8"), "ISO-8859-1"));
		}catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		
		return new ResponseEntity<Resource>(resource, headers, HttpStatus.OK);
	}

- MIME 타입은 다운로드를 할 수 있는 'application/octet-stream'으로 지정하고, 다운로드 시 저장되는 이름은 'Content-Disposition'을 이용해서 지정한다.

- 파일 이름에 대한 문자열 처리는 파일 이름이 한글인 경우 저장할 때 깨지는 문제를 막기 위해서이다. 

- 크롬 브라우저에서 파일 업로드 폴더에 있는 파일의 이름과 확장자로 '/download?fileName=xxxx'와 같이 호출하면 브라우저는 자동으로 해당 파일을 다운로드하는 것을 볼 수 있다.

- IE 계열에서는 파일 다운로드가 호출이 안되는 문제가 발생한다. 이에 대한 처리는 조금 뒤에 살펴보도록 하자.

 

24.1.1 IE/Edge 브라우저의 문제 

- 첨부파일의 다운로드 시 크롬 브라우저와 달리 IE에서는 한글 이름이 제대로 다운로드 되지 않는다. 이것은 'Content-Disposition'의 값을 처리하는 방식이 IE의 경우 인코딩 방식이 다르기 때문이다.

- IE를 같이 서비스해야 한다면 HttpServletRequest에 포함된 헤더 정보들을 이용해서 요청이 발생한 브라우저가 IE 계열인지 확인해서 다르게 처리하는 방식으로 처리한다. HTTP 헤더 메시지 중에서 디바이스의 정보를 알 수 있는 헤더는 'User-Agent' 값을 이용한다. 

(이를 이용해서 브라우저의 종류나 모바일인지 데스크톱인지 혹은 브라우저 프로그램의 종류를 구분할 수 있다.)

 

- 기존의 downloadFile()은 'User-Agent' 정보를 파라미터로 수집하고, IE에 대한 처리를 추가한다. Edge 브라우저는 IE와 또 다르게 처리되므로 주의한다.

// 첨부파일을 다운로드한다. (이미지가 아닌 일반 파일)
	@GetMapping(value = "/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
	@ResponseBody
	public ResponseEntity<Resource> downloadFile(@RequestHeader("User-Agent") String userAgent, String fileName){
		
		log.info("download file:" + fileName);
		Resource resource = new FileSystemResource("파일 업로드 경로" + fileName);
		log.info("resource: " + resource);
		
		if(resource.exists() == false) {
			return new ResponseEntity<>(HttpStatus.NOT_FOUND);
		}
		
		String resourceName = resource.getFilename();
		
		HttpHeaders headers = new HttpHeaders();
		
		try {
			
			String downloadName = null;
			
			if(userAgent.contains("Trident")) { // Trident: IE 브라우저의 엔진 이름 - IE11 처리 
				log.info("IE browser");
				downloadName = URLEncoder.encode(resourceName, "UTF-8").replaceAll("\\+", " ");
			} else if(userAgent.contains("Edge")) {
				log.info("Edge browser");
				downloadName = URLEncoder.encode(resourceName, "UTF-8");
				log.info("Edge name:" + downloadName);
			} else {
				log.info("Chrome browser");
				downloadName = new String(resourceName.getBytes("UTF-8"), "ISO-8859-1");
			}
			
			headers.add("Content-Disposition", "attachment; filename=" + new String(resourceName.getBytes("UTF-8"), "ISO-8859-1"));
			
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		
		return new ResponseEntity<Resource>(resource, headers, HttpStatus.OK);
	}

- downloadFile()은 @RequestHeader를 이용해서 필요한 HTTP 헤더 메시지의 내용을 수집할 수 있다. 이를 이용해서 'User-Agent'의 정보를 파악하고, 값이 'MSIE' 또는 'Trident'인 경우에는 다른 방식으로 처리하도록 한다. 

 

- 위의 코드가 적용되면 우선은 크롬에서 한글 파일의 다운로드를 먼저 시도한 후에 인터넷 등을 이용해서 URL 주소로 인코딩하는 페이지를 이용해서 파일 이름을 변환해 본다. IE에서 주소창에 한글을 적으면 400 Bad Request 에러가 발생한다. 

- IE에서 테스트를 진행하고 싶다면 URL Encoding 작업을 해야 하는데 검색을 통해서 웹페이지를 쉽게 찾을 수 있다. 

- IE 주소창에서는 한글이 직접 처리되지 않으므로, URL ENCODE 사이트에서 변환한 문자열로 호출한다. - 실행 결과를 보면 IE와 크롬 모두 정상적으로 파일 이름이 반영되는 것을 볼 수 있다.- Edge 브라우저의 경우는 크롬과 유사하지만 다음과 같이 다르게 처리한다.* userAgent 내에 'Edge'라는 문자열이 있는지 확인한다. * 다운로드하는 파일 이름에 'ISO-8859-1' 인코딩을 적용하지 않는다. 

 

24.1.2 업로드된 후 다운로드 처리 - 다운로드 자체에 대한 처리는 완료되었으므로, /uploadAjax 화면에서 업로드된 후 파일 이미지를 클릭한 경우에 다운로드가 될 수 있도록 처리한다. - 이미지 파일이 아닌 경우는 아래와 같이 첨부파일 아이콘(이미지)이 보이게 된다. 

- 위의 화면이 나오도록 처리되는 자바스크립트 부분 중 수정되어야 하는 부분은 'attach.png' 파일을 클릭하면 다운로드에 필요한 경로와 UUID가 붙은 파일 이름을 이용해서 다운로드가 가능하도록 <a> 태그를 이용해서 '/download?fileName=xxxx' 부분을 추가한다. - 브라우저에서는 <img> 태그를 클릭하게 되면 자동으로 다운로드가 되는 것을 확인할 수 있다. 

- 다운로드가 정상적으로 이루어지는 것을 확인하였다면 마지막으로 서버에서 파일 이름에 UUID가 붙은 부분을 제거하고 순수하게 다운로드되는 파일의 이름으로 저장될 수 있도록 한다. UploadController의 downloadFile() 메서드를 수정한다.- 수정된 부분은 resourceOriginalName을 생성해서 UUID 부분을 잘라낸 상태의 파일 이름으로 저장하도록 하는 것이다. 브라우저에서는 순수한 파일 이름으로 다운로드 되는 것을 확인할 수 있다.  

 

24.2 원본 이미지 보여주기 

- 일반 첨부파일과 달리 섬네일이 보여지는 이미지 파일의 경우 섬네일을 클릭하면 원본 이미지를 볼 수 있게 처리한다.- 섬네일의 이미지가 '업로드된 경로 + /s_ + UUID_ + 파일 이름'이었다면, 원본 이미지의 이름은 중간에 '/s_'가 '/'로 변경되는 점이 다르다. - 원본 이미지를 화면에서 보기 위해서는 <div>를 생성하고, 해당 <div>에 이미지 태그를 작성해서 넣어주는 작업과 이를 화면상에서 절대 위치를 이용해서 보여줄 필요가 있다. 

 

24.2.1 원본 이미지를 보여줄 <div> 처리 - 이미지의 경우 일반 파일과 달리 이미 이미지 파일 데이터는 섬네일과 동일한 방식으로 처리될 수 있기 때문에 사실상 핵심적인 부분은 이미지를 보여주는 <div>를 처리하는 부분이다. - <div> 를 처리하는 부분은 섬네일 파일을 클릭할 때 이루어 지도록 uploadAjax.jsp 에서 JavaScript 함수 showImage() 를 작성한다. 

- showImage()는 jQuery의 $(document).ready()의 바깥쪽에 작성한다. 이렇게 하는 이유는 나중에 <a> 태그에서 직접 showImage()를 호출할 수 있는 방식으로 작성하기 위해서이다.- 섬네일 이미지를 보여주도록 처리하는 자바스크립트 코드에서는 섬네일의 클릭 시 showImage()가 호출될 수 있는 코드를 추가한다.

// 첨부파일 이름을 목록으로 처리한다.
	var uploadResult = $(".uploadResult ul");
	
	function showUploadedFile(uploadResultArr){
		
		var str = "";
		
		$(uploadResultArr).each(function(i, obj){
			
			if(!obj.image) {
				
				var fileCallPath = encodeURIComponent(obj.uploadPath + "/" + obj.uuid+ "_" + obj.fileName);
				
				console.log(obj.uploadPath);
				
				str += "<li><a href='/download?fileName="+ fileCallPath+ "'>" 
				+ "<img src='/resources/img/attach.png'>" + obj.fileName + "</a></li>";
			} else {
				// str +="<li>" + obj.fileName + "</li>";
				
				var fileCallPath = encodeURIComponent(obj.uploadPath + "/s_" + obj.uuid+ "_" + obj.fileName);
			
				var originPath = obj.uploadPath+ "\\" + obj.uuid + "_" +obj.fileName;
				
				originPath = originPath.replace(new RegExp(/\\/g), "/");
				
				console.log(obj.uploadPath);
				str += "<li><a href=\"javascript:showImage(\'"+originPath+"\')\"><img src='/display?fileName=" + fileCallPath + "'></li>";
			}
		});
		
		uploadResult.append(str);
		
	}

 

- 이미지 첨부파일의 경우 업로드된 경로와 UUID가 붙은 파일의 이름이 필요하기 때문에 originPath라는 변수를 통해서 하나의 문자열로 생성한다. - 생성된 문자열은 '\' 기호 때문에 일반 문자열과는 다르게 처리되므로, '/'로 변환한 뒤 showImage()에 파라미터로 전달한다. 

1) CSS와 HTML 처리 - 실제 원본 이미지를 보여주는 영역의 CSS 스타일을 uploadAjax.jsp 내에 작성한다. (파일 내 코드 또한 참고하기) 

<style>
.bigPictureWrapper {
  position: absolute;
  display: none;
  justify-content: center;
  align-items: center;
  top:0%;
  width:100%;
  height:100%;
  background-color: gray; 
  z-index: 100;
}

.bigPicture {
  position: relative;
  display:flex;
  justify-content: center;
  align-items: center;
}
</style>

 

- 실제 이미지는 '.bigPicture' 안에 <img> 태그를 생성해서 넣게 된다. 이때 CSS의 flex 기능을 이용하면 화면의 정중앙에 배치하는 것은 손쉽게 처리된다. 

- showImage() 함수에서는 약간의 코드를 이용해서 화면에 원본 이미지를 보여줄 수 있도록 수정한다. - showImage()는 내부적으로 화면 가운데 배치하는 작업 후 <img> 태그를 추가하고, jQuery의 animate()를 이용해서 지정된 시간 동안 화면에서 열리는 효과를 처리한다. 첨부파일의 섬네일을 클릭하면 회색 화면의 배경이 깔리고 원본 이미지가 출력되는 것을 볼 수 있다.

 

2) <div> 이벤트 처리 

- 원본 이미지가 보여지는 <div>는 전체 화면을 차지하기 때문에 다시 한번 클릭하면 사라지도록 이벤트를 처리한다.

<div class="bigPictureWrapper">
	<div class="bigPicture">
	</div>
</div>

	
<button id='uploadBtn'>upload</button>


<script>


//이미지 파일의 원본을 보여줄 <div>를 처리한다.
	function showImage(fileCallPath){
	  
	  //alert(fileCallPath);
	
	  $(".bigPictureWrapper").css("display","flex").show();
	  
	  $(".bigPicture")
	  .html("<img src='/display?fileName="+ encodeURI(fileCallPath)+"'>")
	  .animate({width:'100%', height: '100%'});

	}

// 원본 이미지가 띄워진 전체 화면 클릭 시 사라지는 이벤트 
	$(".bigPictureWrapper").on("click", function(e){
	  $(".bigPicture").animate({width:'0%', height: '0%'});
	  setTimeout(function() {
		  $(".bigPictureWrapper").hide();
	  });

	});

 

24.3 첨부파일 삭제 

- 첨부파일 삭제는 생각보다 많은 고민이 필요한 작업이다. 단순히 파일 하나만을 삭제한다고 생각할 수 있지만 실제로는 다음과 같은 문제점들을 고민해야 한다.* 이미지 파일의 경우에는 섬네일까지 같이 삭제되어야 하는 점* 파일을 삭제한 후에는 브라우저에서도 섬네일이나 파일 아이콘이 삭제되도록 처리하는 점 * 비정상적으로 브라우저의 종료 시 업로드된 파일의 처리 

 

24.3.1 일반 파일과 이미지 파일의 삭제 - 업로드된 첨부파일의 삭제는 Ajax를 이용하거나 <form> 태그를 이용하는 방식 모두를 적용할 수 있다. - 이미 업로드된 첨부파일의 삭제는 일반 파일의 경우에는 업로드된 파일만을 삭제하면 되지만, 이미지의 경우에는 생성된 섬네일 파일과 원본 파일을 같이 삭제해야 한다. 

 

- 서버 측에서는 삭제하려는 파일의 확장자를 검사해서 일반 파일인지 이미지 파일인지를 파악하거나 파라미터로 파일의 종류를 파악하고, 이를 이용해서 처리를 다르게 한다. 

 

1) 화면에서 삭제 기능 

- 첨부파일이 업로드된 후에 생기는 이미지 파일 옆에 'x' 표시를 추가하도록 코드를 수정한다. - 변경된 부분은 <span> 태그를 이용해서 섬네일이나 파일 아이콘 옆에 'x' 표시를 추가한 점과 <span> 태그에 'data-file'과 'data-type' 속성을 추가한 부분이다. 화면을 보면 디자인은 볼품없지만, 테스트할 수 있도록 만든 'x' 표시가 보인다. 

'x' 표시에 대한 이벤트 처리는 아래와 같이 작성한다.

// 첨부파일 옆 x 클릭시 파일을 삭제 한다.
	$(".uploadResult").on("click", "span", function(e){
		
		var targetFile = $(this).data("file");
		var type = $(this).data("type");
		console.log(targetFile);
		
		$.ajax({
			url: '/deleteFile',
			data: {fileName: targetFile, type:type},
			dataType: 'text',
			type:'POST',
			success: function(result){
				alert(result);
			}
		});
	});

- 첨부파일의 삭제는 <span> 태그를 이용해서 처리하지만, 첨부파일의 업로드 후에 생성되기 때문에 '이벤트 위임'방식으로 처리해야 한다. 이벤트 처리에서는 Ajax를 이용해서 첨부파일의 경로와 이름, 파일의 종류(이미지 혹은 일반)을 전송한다. 

 

2) 서버에서 첨부파일의 삭제 

- 서버 측에서 첨부파일은 전달되는 파라미터의 이름과 종류를 파악해서 처리한다. - upLoadController에 deleteFile() 메서드를 추가한다. 

@PostMapping("/deleteFile")
	@ResponseBody
	public ResponseEntity<String> deleteFile(String fileName, String type) {

		log.info("deleteFile: " + fileName);

		File file;

		try {
			file = new File("파일 업로드 경로" + URLDecoder.decode(fileName, "UTF-8"));

			file.delete();

			if (type.equals("image")) {

				String largeFileName = file.getAbsolutePath().replace("s_", "");

				log.info("largeFileName: " + largeFileName);

				file = new File(largeFileName);

				file.delete();
			}

		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
			return new ResponseEntity<>(HttpStatus.NOT_FOUND);
		}

		return new ResponseEntity<String>("deleted", HttpStatus.OK);

	}

- deleteFile()은 브라우저에서 전송하는 파일 이름과 종류를 파라미터로 받아서 파일의 종류에 따라 다르게 동작한다. - 브라우저에서 전송되는 파일 이름은 '경로 + UUID + _ + 파일 이름' 으로 구성되어 있으므로, 일반 파일의 경우에는 파일만을 삭제한다.- 이미지의 경우 섬네일이 존재하므로, 파일 이름의 중간에 's_'가 들어가있다. 일반 이미지 파일의 경우 's_'가 없도록 되어 있으므로 이 부분을 변경하여 원본 이미지 파일도 같이 삭제하도록 처리한다. 

 

24.3.2 첨부파일의 삭제 고민

- 첨부파일을 삭제하는 작업의 최대 고민은 사용자가 비정상적으로 브라우저를 종료하고 나가는 행위이다. 서버에는 Ajax를 이용해서 업로드했기 때문에 이미 저장이 된 상태지만, 사용자가 '작업 관리자'나 전원 버튼을 누르는 등의 조치를 해서 브라우저 자체를 종료해 버린다면 이를 감지할 수 있는 적당한 방법이 없다. 

- 이에 대한 가장 좋은 해결책은 실제 최종적인 결과와 서버에 업로드된 파일의 목록을 비교해서 처리하는 것이다.

- 보통 이런 작업은 spring-batch나 Qaurtz 라이브러리를 이용해서 처리한다. 자세한 내용은 뒤쪽에서 다룬다.