파일을 업로드하고 다운로드하는 방법에 대해 알아보려고 한다.
1. 파일 업로드
1) HTML
파일 업로드는 form 데이터로 쉽게 업로드할 수 있다.
HTML 태그를 다음과 같이 작성한다.
<input type="file" name="file" id="imageFileOpenInput" accept="image/*">
파일도 이렇게 input 태그로 데이터를 전송할 수 있다. (type은 file이다.)
name으로 이름을 지정하면, 서버에선 이 이름으로 데이터를 얻을 것이다.
accept는 전송 허용 가능한 파일의 타입을 지정하는 것이다.
예제 코드에서는 모든 이미지 파일을 허용하고 있다.
<input type="file" name="reviewImg" id="reviewImageFileOpenInput" accept="image/png, image/jpeg">
이런 식으로 이미지 파일 중 png, jpeg 확장자의 파일만 허용하겠다고 할 수도 있다.
콤마로 여러 타입을 지정할 수 있다.
참고로 accept 속성의 브라우저 지원 범위는 별로 좋지 않다.
Can I use 사이트에서 더 자세한 내용을 확인할 수 있다.
(출처: Can I use - accept attribute for file input )
이 HTML 소스는 웹 페이지 화면에 다음과 같이 나온다.
input type="file" 태그는 이런 UI를 기본적으로 제공하고 있다.
파일 선택 버튼을 누르면,
이런 파일을 선택할 수 있는 창이 뜬다.
accept 속성을 이미지 파일만 가능하도록 설정했기 때문에, 이미지 파일만 선택할 수 있다.
업로드한 파일을 선택하면,
이렇게 선택된 파일명이 표시된다.
하지만 아직 서버로 파일이 전송된 것은 아니다.
form 태그에 method 속성 값을 post, action에 form 데이터를 전송할 경로, enctype에 multipart/form-data를 써줘야 한다. (enctype을 multipart/form-data로 지정하여 이 form은 multipart를 포함하고 있다고 알려주는 것이다)
<form method="post" action="upload" enctype="multipart/form-data">
<div>
file : <input type="file" name="file" accept="image/*">
</div>
<input type="submit">
</form>
그리고 submit 버튼을 만들고,
파일 선택 후 제출 버튼을 누른다면 서버로 파일 데이터가 전송된다.
제출 후, 크롬 개발자 도구의 Network 탭을 확인해본다.
Request Header 중 Content-Type을 확인해보면 다음과 같이 나온다.
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryRApOxiG9s3BqghQQ
이렇게 전송하고 서버에서 multipart/form-data를 처리할 수 있도록 만들면 된다.
세미콜론(;) 이후에 boundary는 경계라는 뜻이다.
브라우저에서 서버로 데이터를 전송될 때 데이터는 다음과 같은 형태를 하고 있다.
------WebKitFormBoundaryRApOxiG9s3BqghQQ
Content-Disposition: form-data; name="file"; filename="uploadimage.png"
Content-Type: image/png
바로 WebKitFormBoundaryRApOxiG9s3BqghQQ라는 구분 정보를 기준으로 데이터를 구분한다는 의미이다.
2) JavaScript
위에서 말했듯이 input type file 태그의 accept 속성의 브라우저 지원 범위가 별로 좋지 않다.
자바스크립트로 더 많은 브라우저에서 동작하는 코드를 만들 수 있다.
const elImage = document.querySelector("#reviewImageFileOpenInput");
elImage.addEventListener("change", (evt) => {
const image = evt.target.files[0];
if(!validImageType(image)) {
console.warn("invalide image file type");
return;
}
});
function valideImageType(image) {
const result = ([ 'image/jpeg',
'image/png',
'image/jpg' ].indexOf(image.type) > -1);
return result;
}
file 업로드 시, change 이벤트로 이를 감지할 수 있다.
이때 event 객체의 target으로부터 file 객체를 얻을 수 있다.
이 file 객체의 타입을 valideImageType 메서드로 검사한다.
이미지를 업로드할 때 어떤 이미지인지 알려주는 썸네일 기능을 제공해주려면,
Ajax로 이미지 같은 파일을 먼저 서버로 전송하고, 잘 전송되면 받는 응답 값에서 이미지 주소를 받아 썸네일로 제공할 수 있다.
여기선 일단 편의상 서버로 올리기 전, createObjectURL을 사용하여 썸네일을 노출하는 방법만 알아보았다.
const elImage = document.querySelector(".thumb_img");
elImage.src = window.URL.createObjectURL(image);
썸네일 이미지를 노출시킬 img 엘리먼트를 선택하고,
그 img 태그의 src 값을 이미지 url 정보로 지정한다.
file upload를 Ajax기술로 구현할 수 있다.
약간 더 복잡한 처리를 해야 하지만, FormData라는 속성을 이용하면 좀 더 쉽게 구현할 수가 있다.
이 부분은 나중에 좀 더 자세히 정리하려고 한다.
3) JAVA
위에서 언급했듯이 클라이언트에서 서버로 파일을 전송할 땐 Multipart로 전송한다.
Multipart는
이렇게 웹 클라이언트가 요청을 보낼 때 HTTP 프로토콜의 바디 부분에 데이터를 여러 부분으로 나눠서 보내는 것이다.
HttpServletRequest는 Multipart 데이터를 쉽게 처리하는 메서드를 지원하지 않는다.
HttpServletRequest는 HTTP 프로토콜의 바디 부분을 읽어 들이는 InputStream만을 지원하고 있다.
이런 InputStream으로 multipart 부분을 잘 나눠 사용해야 한다.
하지만 이를 직접 구현할 필요는 없고, 별도의 라이브러리를 사용하면 된다.
대표적으로 아파치 재단의 commons-fileupload를 많이 사용한다.
Spring MVC에서 파일 업로드하는 예제를 작성하도록 하겠다.
먼저 pom.xml에 commons-fileupload 라이브러리와 commons-io 라이브러리 의존성을 추가한다.
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>1.4</version>
</dependency>
스프링 설정 파일에 다음 소스를 추가하여, DispatcherServlet이 준비 과정에서 multipart/form-data가 요청으로 올 때 다음 MultipartResolver를 사용하도록 한다.
@Bean
public MultipartResolver multipartResolver() {
org.springframework.web.multipart.commons.CommonsMultipartResolver multipartResolver = new org.springframework.web.multipart.commons.CommonsMultipartResolver();
multipartResolver.setMaxUploadSize(10485760); // 1024 * 1024 * 10
return multipartResolver;
}
HTML 코드는 위에서 했던 것처럼 form 태그의 enctype을 multipart/form-data, method는 post로 지정한다.
만약 type이 fiel인 input 태그가 여러 개 있고, name 속성의 값이 같다면, file이 배열 형태로 컨트롤러에 전달된다.
이제 form으로부터 전달되는 파일 정보를 받는 Controller가 필요하다.
package kr.or.connect.guestbook.controller;
import java.io.FileOutputStream;
import java.io.InputStream;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
@Controller
public class FileController {
// get방식으로 요청이 올 경우 업로드 폼을 보여준다.
@GetMapping("/uploadform")
public String uploadform() {
return "uploadform";
}
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
System.out.println("파일 이름 : " + file.getOriginalFilename());
System.out.println("파일 크기 : " + file.getSize());
try(
// 맥일 경우
//FileOutputStream fos = new FileOutputStream("/tmp/" + file.getOriginalFilename());
// 윈도우일 경우
FileOutputStream fos = new FileOutputStream("c:/tmp/" + file.getOriginalFilename());
InputStream is = file.getInputStream();
){
int readCount = 0;
byte[] buffer = new byte[1024];
while((readCount = is.read(buffer)) != -1){
fos.write(buffer,0,readCount);
}
}catch(Exception ex){
throw new RuntimeException("file Save Error");
}
return "uploadok";
}
}
input type file의 name이 file이었으므로, file이란 이름으로 파라미터를 받는다.
클라이언트에서 전송한 파일 데이터를 MultipartFile 객체로 받게 된다.
이 객체의 getOriginalFilename()으로 파일 이름을, getSize()로 파일의 크기를 구할 수 있다.
위 코드 상엔 없지만 이렇게 얻어온 데이터를 데이터베이스에 저장하면 된다.
이제 실제 파일을 입력해오기 위해, getInputStream()이란 메서드를 사용한다.
file.getInputStream()으로 파일을 얻어올 수 있는 통로를 받고,
read 메서드와 write 메서드로 파일을 저장한다.
위 코드에선 c:/tmp에 파일을 저장하도록 하고 있다.
참고로 여기서 사용자가 올린 파일의 이름으로 파일을 저장하도록 하고 있는데,
여러 사용자가 사용할 때, 똑같은 이름의 파일을 업로드할 수 있으므로, 뒤에 시간을 붙이는 등 중복을 고려하는 프로그래밍을 해야 한다.
2. 파일 다운로드
서버의 파일을 다운로드하여 사용자에게 제공하는 방법을 알아보려고 한다.
Controller에서 다운로드 기능을 구현하면 된다.
파일 다운로드는 파일 데이터를 가져오는 것이므로, @GetMapping을 사용한다.
@GetMapping("/download")
public void download(HttpServletResponse response) {
// 직접 파일 정보를 변수에 저장해 놨지만, 이 부분이 db에서 읽어왔다고 가정한다.
String fileName = "connect.png";
String saveFileName = "c:/tmp/connect.png"; // 맥일 경우 "/tmp/connect.png" 로 수정
String contentType = "image/png";
int fileLength = 1116303;
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\";");
response.setHeader("Content-Transfer-Encoding", "binary");
response.setHeader("Content-Type", contentType);
response.setHeader("Content-Length", "" + fileLength);
response.setHeader("Pragma", "no-cache;");
response.setHeader("Expires", "-1;");
try(
FileInputStream fis = new FileInputStream(saveFileName);
OutputStream out = response.getOutputStream();
){
int readCount = 0;
byte[] buffer = new byte[1024];
while((readCount = fis.read(buffer)) != -1){
out.write(buffer,0,readCount);
}
}catch(Exception ex){
throw new RuntimeException("file Save Error");
}
}
위 코드에서는 파일 정보를 직접 입력했지만, 실제로는 데이터베이스에서 가져와야 한다.
클라이언트에서 서버로 요청을 보낼 때 파일 정보를 찾을 수 있는 id 등의 값을 보내고,
이 id 등의 값을 이용하여 데이터베이스에서 맞는 파일 정보를 가져오도록 해야 한다는 것이다.
아무튼 이렇게 가져온 파일 정보를 response의 setHeader() 메서드로 파일명, 파일 타입, 파일 길이를 지정한다.
브라우저가 캐시를 읽지 못하도록 no-cache로 설정하고,
이런 헤더 정보로 전송을 한다.
이제 이 response로부터 OutputStream을 이용해 읽어드린 내용의 파일을 출력하도록 한다.
실행해보면 파일이 다운로드되는 것을 확인할 수 있을 것이다.
출처)
edwith 부스트코스 웹 프로그래밍
- 6. 웹 앱 개발: 예약서비스 4/4의 파일 업로드 - FE, 파일 업로드 & 다운로드 - BE
'etc > Web' 카테고리의 다른 글
로컬 테스트 서버 Python의 SimpleHTTPServer (0) | 2019.10.07 |
---|---|
웹 프론트엔드 개발자가 공부할 것들 (0) | 2019.09.10 |
웹이 동작하는 법 (HTTP 프로토콜의 이해) (0) | 2019.08.20 |
웹 프로그래밍을 위한 프로그래밍 언어 (0) | 2019.08.20 |
댓글