본문 바로가기
Java/Spring

Spring) 파일 다운로드

by 박채니 2022. 9. 4.
안녕하세요, 코린이의 코딩 학습기 채니 입니다.
개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.

 

파일 다운로드

 

boardDetail.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<jsp:include page="/WEB-INF/views/common/header.jsp">
	<jsp:param value="게시판 상세보기" name="title"/>
</jsp:include>
<style>
div#board-container{width:400px;}
input, button, textarea {margin-bottom:15px;}
button { overflow: hidden; }
/* 부트스트랩 : 파일라벨명 정렬*/
div#board-container label.custom-file-label{text-align:left;}
</style>
<div id="board-container" class="mx-auto text-center">
	<input type="text" class="form-control" 
		   placeholder="제목" name="boardTitle" id="title" 
		   value="${board.title}" required>
	<input type="text" class="form-control" 
		   name="memberId" 
		   value="${board.member.name}" readonly required>
	
	<c:if test="${not empty board.attachments}">
		<c:forEach items="${board.attachments}" var="attachment" varStatus="vs">
			<button type="button" 
					class="btn btn-outline-success btn-block" onclick="location.href='${pageContext.request.contextPath}/board/fileDownload.do?no=${attachment.no}'">
				첨부파일${vs.count} - ${attachment.originalFilename}
			</button>			
		</c:forEach>
	</c:if>
	
    <textarea class="form-control" name="content" 
    		  placeholder="내용" required>${board.content}</textarea>
    <input type="number" class="form-control" name="readCount" title="조회수"
		   value="${board.readCount}" readonly>
	<input type="datetime-local" class="form-control" name="created_at" 
		   value='${board.createdAt}'>
</div>
<jsp:include page="/WEB-INF/views/common/footer.jsp"></jsp:include>

 

Controller (Service - Dao 생략)

BoardController

 

Resource : 다음 구현체들의 추상화레이어를 제공 (인터페이스)

 

웹 상 자원 : UrlResponse

classpath 자원 : ClassPathResource

서버컴퓨터 자원 : FileSystemResource

ServletContext(web root) 자원 : ServletContextResource

입출력자원 : InputStreamResource

이진데이터 자원 : ByteArrayResource

 

@ResponseBody : 핸들러의 반환된 자바 객체를 응답메세지 바디에 직접 출력하는 경우 사용

@Autowired
ResourceLoader resourceLoader;

@GetMapping("/fileDownload.do")
// 이진데이터가 작성된 응답이 올 것을 기대하고 브라우저는 그것을 읽어서 본인 컴퓨터에 저장!!
@ResponseBody
public Resource fileDownload(@RequestParam int no, HttpServletResponse response) throws IOException {
    Attachment attach = boardService.selectOneAttachment(no);
    log.debug("attach = {}", attach);

    String saveDirectory = application.getRealPath("/resources/upload/board");
    File downFile = new File(saveDirectory, attach.getRenamedFilename());
    // "file:" -> 프로토콜 접두사 (사용 시 FileSystemResource 출동!!)
    String location = "file:" + downFile;	// File#toString은 파일의 절대경로를 반환
    log.debug("location = {}", location);
    Resource resource = resourceLoader.getResource(location); // 해당 자원에 대한 실제 구현체를 만들어서 반환!
    log.debug("resource = {}", resource);
    log.debug("resource#file = {}", resource.getFile());

    // 응답헤더 작성 -> 브라우저가 이를 알고 다운로드 받을 준비 + 다운로드 받을 파일이름 지정
    response.setContentType("application/octet-stream; charset=utf-8"); // 이진데이터 임을 명시
    String filename = new String(attach.getOriginalFilename().getBytes("utf-8"), "iso-8859-1");
    response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename +"\"");

    return resource;
}

@콘솔출력값
DEBUG: com.ce.spring2.board.controller.BoardController - attach = Attachment(no=21, boardNo=62, originalFilename=2022-02-24.png, renamedFilename=20220830_150030667_393.png, downloadCount=0, createdAt=2022-08-30T15:00:30)
DEBUG: com.ce.spring2.board.controller.BoardController - location = file:C:\Workspaces\spring_workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp1\wtpwebapps\hello-spring2\resources\upload\board\20220830_150030667_393.png
DEBUG: com.ce.spring2.board.controller.BoardController - resource = URL [file:C:/Workspaces/spring_workspace/.metadata/.plugins/org.eclipse.wst.server.core/tmp1/wtpwebapps/hello-spring2/resources/upload/board/20220830_150030667_393.png]
DEBUG: com.ce.spring2.board.controller.BoardController - resource#file = C:\Workspaces\spring_workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp1\wtpwebapps\hello-spring2\resources\upload\board\20220830_150030667_393.png

해당 파일의 filename을 가져오기 위하여 DB에서 Attachment 객체를 가져왔습니다.

그 후 해당 위치의 File객체를 생성하여 "file:" 프로토콜 접두사를 이용해 FileSystemResource를 출동!!시킵니다.

ResourceLoader로부터 해당 파일의 절대 경로에 대해 실제 구현체를 만들어서 Resource 객체를 반환해줍니다.

 

* 응답헤더 작성

한글파일을 대비하여 filename 생성

getBytes()로 생성(utf-8 방식으로 쓰인)

"iso-8859-1" 방식으로 인코딩 (톰캣에 의해 전송되면서 깨지지 않음!)

 

생성된 filename을 Header에 추가해주면 다운로드 처리 완료!

 

 

※ CONTENT-DISPOSITION 관련 

https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Content-Disposition 

 

Content-Disposition - HTTP | MDN

multipart/form-data 본문에서의 Content-Disposition 일반 헤더는 multipart의 하위파트에서 활용될 수 있는데, 이 때 이 헤더는 multipart 본문 내의 필드에 대한 정보를 제공합니다. multipart의 하위파트는 Content

developer.mozilla.org

 

다운로드는 잘 되지만 네트워크 상의 응답 헤더를 확인해보면 Content-Type이 설정한 타입이 아닌 'text/html'로 받아온 것을 확인할 수 있습니다.

그 이유는 작성 이후 spring이 content-type를 뒤집어 씌웠기 때문입니다. (오버라이딩 처리)

 

따라서 직접 명시를 해줘야할 필요가 있습니다.

produces = MediaType.APPLICATION_OCTET_STREAM_VALUE 처리

// produces : 핸들러가 어떤 것을 생성하는 지 명시해줌
@GetMapping(path = "/fileDownload.do", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)

이진데이터임을 파악하고 실제 타입 확장자별로 mediaType을 지정해준 것을 확인할 수 있습니다.

 

 

 

※ 파일이름이 한글로 된 파일 다운로드 시

한글을 "iso-8859-1"로 인코딩 처리했기 때문에 Content-Disposition에는 깨진 파일이름이 보여지지만, 크롬에서 역으로 디코딩할 때는 한글로 올바르게 출력됩니다.