File Upload / Download

스프링을 이용한 파일 업로드 / 다운로드 구현

Spring File Upload 구현

Dependency 추가

Spring에서 파일 업로드를 구현하기 위해서는 MultipartResolver의 구현체가 필요하다. 여기서는 Apache Common IO, Fileupload를 이용한다.

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>

MultipartResolver 선언

CommonMultipartResolver를 DispatcherServlet(servlet-context.xml)에 등록한다.

<beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
	<beans:property name="defaultEncoding" value="UTF-8"></beans:property>
	<beans:property name="maxUploadSize" value="10000000"></beans:property>
</beans:bean>

반드시 등록할 ID는 multipartResolver로 설정해야 한다.

파일 업로드 화면 구현

파일 업로드를 위해 업로드 화면을 구현한다.

  • /upload[GET]을 처리할 컨트롤러 매핑 생성

  • 연결된 JSP 페이지

Controller

Mapping : com.hakademy.spring10.controller.MultipartController

@GetMapping("/upload")
public String upload() {
	return "upload";
}

View

View : /WEB-INF/views/upload.jsp

<h1>upload1 : 파일 1개만 등록</h1>
<form action="upload1" method="post" enctype="multipart/form-data">
	<input type="file" name="myfile">
	<input type="submit" value="등록">
</form>

파일 업로드 서버측 처리

파일을 서버에서 수신하기 위해서 사용할 수 있는 방법은 여러 가지가 있다.

MultipartRequest를 이용하여 수신하는 방법

/upload1을 처리할 수 있는 매핑을 생성하여 MultipartRequest로 처리한다.

@PostMapping("/upload1")
@ResponseBody
public String upload1(MultipartRequest mRequest) {
	return "upload complete";
}

업로드 테스트를 수행했을 때 화면에 "upload complete"라는 메시지가 나온다면 성공이다.

서버를 키고 http://localhost:8080/spring10/upload에 접속하여 업로드 테스트를 수행한다.

현재 파일 저장이나 파일 정보 분석은 아무것도 이루어지지 않은 상태이기 때문에 추가 코드를 작성해야 한다. MultipartRequest에서 파일과 관련하여 사용할 수 있는 명령은 다음과 같다.

  • getFile(String name) : MultipartFile 파라미터명을 이용하여 업로드한 파일을 MultipartFile 형태로 추출한다. 단일 파일 전송인 경우 사용한다.

  • getFiles(String name) : List 파라미터명을 이용하여 업로드한 파일을 List 형태로 추출한다. multiple 형태의 파일 전송인 경우 사용한다.

  • getFileNames() : Iterator 전송된 파일들의 이름을 모두 추출한다.

  • getFileMap() : Map<String, MultipartFile> 전송된 모든 파일들을 [전송이름=파일] 형태로 추출한다. 파일 여러개를 전송하지만 multiple 전송이 아닌 경우 사용한다.

  • getMultiFileMap() : MultiValueMap<String, List> 전송된 모든 파일들을 [전송이름=파일목록] 형태로 추출한다. 파일 여러개를 multiple 전송하는 경우까지 처리가 가능하다.

지금 예제에서는 파일을 1개만 전송하기 때문에 getFile() 을 이용하여 처리할 수 있다.

MultipartFile file = mRequest.getFile("myfile");

MultipartFile에는 다음과 같은 명령이 존재한다.

  • .getBytes() : byte[] 파일 내용을 byte 배열로 반환

  • .getContentType() : String 파일 MIME TYPE을 문자열로 반환

  • .getInputStream() : InputStream 파일을 읽을 수 있는 InputStream을 반환

  • .getName() : String 업로드된 parameter name을 반환(파일명이 아님)

  • .getOriginalFilename() : String 업로드된 파일 이름을 반환

  • .getSize() : long 업로드된 파일 크기를 반환

  • .isEmpty() : boolean 파일이 존재하는지 여부를 반환

  • .transferTo(File dest) : void 대상 파일 객체에 내용을 저장(IllegalStateException, IOException 발생)

현재 컨트롤러에서 해야할 일은 파일을 특정 위치에 저장하는 것이므로 내용은 따로 추출할 필요가 없다. 만약 데이터베이스에 같이 저장하는 경우라면 파일의 주요 내용은 데이터베이스에 저장하고 실제 파일의 내용은 물리 저장소에 저장하는 형식으로 구현될 것이다.

저장될 폴더를 생성하는 코드를 작성하고

File dir = new File("C:/upload");
dir.mkdirs();

실제 저장할 파일 객체 업로드된 이름과 동일하게 만든 뒤

File target = new File(dir, file.getOriginalFilename());

실제 저장하는 코드를 작성한다.

file.transferTo(target);

완성된 컨트롤러의 모습은 다음과 같다.

@PostMapping("/upload1")
@ResponseBody
public String upload1(MultipartRequest mRequest) throws IllegalStateException, IOException {
	MultipartFile file = mRequest.getFile("myfile");
	File dir = new File("C:/upload");
	dir.mkdirs();
	File target = new File(dir, file.getOriginalFilename());
	file.transferTo(target);
	return "upload complete";
}

서버를 키고 http://localhost:8080/spring10/upload에 접속하여 업로드 테스트를 수행한다.

실제 경로에 파일이 생성된 것을 확인한다.

경로는 여러가지로 설정할 수 있다.

  • 절대 경로를 지정하는 방법

  • ServletContext를 이용하는 방법

  • 다른 서버로 전송하는 방법

현재 예제에서 살펴본 방법은 "절대 경로를 지정하는 방법"이며, 프로젝트와 독립된 별도의 서버 영역에 저장된다. 또한 별도의 작명 정책을 설정하지 않고 "사용자가 올린 파일 이름 그대로" 저장하기 때문에 같은 이름의 파일이 두 번 이상 업로드 될 경우 기존의 파일에 덮어쓰기된다는 단점을 가지고 있다.

파일명의 중복을 방지하기 위하여 시도할 수 있는 방법은 여러 가지가 있지만, 현재 예제에서는 데이터베이스를 사용하지 않기 때문에 기존 파일명을 기억할 수 없으므로 사용하지 않는다.

MultipartFile 을 이용하는 방법

MultipartRequest에서 추출하여 사용하는 것도 좋은 방법이겠지만 스프링의 자동화 기능을 조금 더 사용하면 편하게 파일을 저장할 수 있다.

파일이 전송되는 경우는 크게 두 가지로 생각할 수 있다.

한 개만 전송되는 경우

아래와 같이 입력창이 구성된 경우 myfile이란 이름으로는 단 한개의 파일만 전송할 수 있다.

<input type="file" name="myfile">

이 때에는 MultipartFile 변수로 데이터를 수신할 수 있다.

@PostMapping("/upload2")
@ResponseBody
public String upload2(@RequestParam MultipartFile myfile){
	...
}

여러 개 전송되는 경우

아래와 같이 입력창이 구성된 경우 myfile이란 이름으로는 여러 개의 파일이 전송될 수 있다.

<input type="file" name="myfile" multiple>

이 때에는 MultipartFile 배열이나 List 형태로 데이터를 수신할 수 있다.

배열을 사용하는 매핑은 다음과 같은 형태를 가진다.

@PostMapping("/upload3")
public String upload3(@RequestParam MultipartFile[] myfile){
	...
}

List를 사용하는 매핑은 다음과 같은 형태를 가진다.

@PostMapping("/upload3")
public String upload3(@RequestParam List<MultipartFile> myfile){
	...
}

완성된 코드는 첨부된 MultipartController/upload2, /upload3, /upload4 코드를 보면 확인할 수 있다.

주의 : 파일이 없을 때는 처리하지 않았기 때문에 오류가 발생한다.

ModelAttribute 와 VO를 이용하는 방법

@ModelAttribute를 이용하면 객체 형태로도 multipart/form-data 요청을 받을 수 있다.

multiple 형식으로 업로드되는 요청을 처리하기 위해 다음과 같은 클래스를 생성한다.

com.hakademy.spring10.vo.MultipartVO

@Data @Builder @NoArgsConstructor @AllArgsConstructor
public class MultipartVO{
	private List<MultipartFile> myfile;
}

컨트롤러는 /test5 를 처리하도록 구현한다.

com.hakademy.spring10.controller.MultipartController

@PostMapping("/upload5")
@ResponseBody
public String upload5(@ModelAttribute MultipartVO vo) {
	...	
}

완성된 코드는 첨부된 MultipartController/upload5 코드를 보면 확인할 수 있다.

Spring File Download 구현

스프링에서 파일 다운로드를 구현하기 위해서는 View Resolver를 무시하고 파일의 내용을 사용자에게 직접 전송해야 하며, 사용자는 이를 다운로드 할 수 있어야 한다.

즉, 사용자에게 파일 다운로드를 준비하도록 형식을 알려줄 필요가 있는데 이 때 Response에 Header를 설정하여 파일 다운로드와 관련된 각종 정보들을 알려준다.

Mapping 생성

/download1, /download2 주소로 접속하면 filename 파라미터에 들어있는 이름에 해당하는 파일 이름을 저장된 위치에서 찾아서 다운로드하도록 매핑을 구성한다.

HttpServletResponse를 이용하여 전송할 경우의 매핑

@GetMapping("/download1")
public void download1(@RequestParam String filename, HttpServletResponse response) {
	...
}

파일명을 파라미터로 받고, 직접 전송을 위한 HttpServletResponse 객체를 선언한다.

ResponseEntity를 이용하여 전송할 경우의 매핑

@GetMapping("/download2")
public ResponseEntity<ByteArrayResource> download2(@RequestParam String filename){
	...
}

파일명을 파라미터로 받고, 스프링에 다운로드 처리를 요청하기 위한 응답객체인 ResponseEntity를 반환형으로 설정한다. 이 때, ByteArrayResource를 같이 첨부해야 하는데 이 클래스는 byte[] 의 Wrapper 형태이다.

파일 로드

파일을 불러오는 코드는 두 가지 형태 모두 동일하다. 전송된 파일 이름을 이용하여 업로드 폴더에서 byte 데이터를 불러와야 하는데, apache common-io를 이용하여 한 번에 불러온다.

기준 폴더 객체를 생성하고

File dir = new File("C:/upload");

기준 폴더 내부의 대상 파일 객체를 생성한다.

File target = new File(dir, filename);

대상 파일 객체에서 apache common io에 있는 FileUtils 클래스 명령을 이용하여 바이트 데이터를 불러온다.

byte[] data = FileUtils.readFileToByteArray(target);

Response Header 설정

Response Header 설정은 다운로드 구현 방식에 따라 다르지만 같은 값을 설정한다.

  • Content-Type 문서의 형식을 설명한다. application/octet-stream로 설정한다.

  • Content-Disposition 브라우저 상에서 파일에 대해 처리할 행동을 정의한다. attachment; filename="파일명"으로 작성한다.

  • Content-Length 파일 크기를 명시한다. 문자열 형태로 작성한다.

  • Content-Encoding 인코딩 방식을 정의한다. UTF-8로 작성한다.

HttpServletResponse를 이용하여 전송할 경우의 매핑

HttpServletResponse를 이용할 경우 Resposne Header는 다음과 같이 설정한다.

response.setHeader("이름", "값");

Content-Type, Content-Disposition, Content-Length 를 설정한 코드는 다음과 같다.

response.setHeader("Content-Type", "application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=\""+target.getName()+"\"");
response.setHeader("Content-Length", String.valueOf(target.length()));
response.setHeader("Content-Encoding", "UTF-8");

오타를 줄이기 위해 HttpHeaders 클래스의 상수를 활용하여 작성하면 다음과 같다.

response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""+target.getName()+"\"");
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(target.length()));
response.setHeader(HttpHeaders.CONTENT_ENCODING, "UTF-8");

한글 이름까지 올바로 표시되게 하고 싶다면 Content-Disposition의 target.getName()UTF-8로 인코딩 해야 한다. (모든 브라우저별로 고려하고 싶다면 브라우저마다 다르게 설정하도록 구성해야 한다)

response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""+URLEncoder.encode(target.getName(), "UTF-8")+"\"");

ResponseEntity를 이용하여 전송할 경우의 매핑

ResponseEntity의 경우에는 다음과 같이 Builder를 이용하여 생성하는 형태이다.

ResponseEntity.ok().header(...).header(...).body(...)

필요한 헤더들을 설정하면 다음과 같은 코드가 된다.

ResponseEntity.ok()
	.header("Content-Type", "application/octet-stream")
	.header("Content-Disposition", "attachment; filename=\""+URLEncoder.encode(target.getName(), "UTF-8")+"\"")
	.header("Content-Length", String.valueOf(target.length()))
	.header("Content-Encoding", "UTF-8")
	.body(...)

HttpHeaders의 상수를 적용시키면 다음과 같다.

ResponseEntity.ok()
	.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE)
	.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""+URLEncoder.encode(target.getName(), "UTF-8")+"\"")
	.header(HttpHeaders.CONTENT_LENGTH, String.valueOf(target.length()))
	.header(HttpHeaders.CONTENT_ENCODING, "UTF-8")
	.body(...)

ResponseEntity의 Builder에서는 축약 명령들을 제공하는데 사용한 코드는 다음과 같다.

ResponseEntity.ok()
	.contentType(MediaType.APPLICATION_OCTET_STREAM)
	.contentLength(target.length())
	.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""+URLEncoder.encode(target.getName(), "UTF-8")+"\"")
	.header(HttpHeaders.CONTENT_ENCODING, "UTF-8")
	.body(...)

.contentType()의 경우 MediaType 상수 객체로 설정이 가능하고, .contentLength()는 long 타입의 데이터를 변환 없이 설정할 수 있도록 되어 있어 편리하다.

.body(...) 부분에는 ByteArrayResource 형태의 데이터를 추가해야 하는데, 없기 때문에 생성해야 한다.

ByteArrayResource resource = new ByteArrayResource(data);

byte[] 을 생성자의 인자로 객체를 생성한 뒤 body 부분에 추가해주면 된다.

완성된 코드는 다음과 같다.

ResponseEntity.ok()
	.contentType(MediaType.APPLICATION_OCTET_STREAM)
	.contentLength(target.length())
	.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""+URLEncoder.encode(target.getName(), "UTF-8")+"\"")
	.header(HttpHeaders.CONTENT_ENCODING, "UTF-8")
	.body(resource)

사용자에게 출력

사용자에게 출력하기 위한 코드도 경우에 따라 다르다.

HttpServletResponse를 이용할 경우의 출력

직접 출력할 경우 Response에 있는 OutputStream을 이용하여 byte 데이터를 전송한다.

response.getOutputStream().write(data);

ResponseEntity를 이용할 경우의 출력

Spring을 이용할 경우 ResponseEntity 객체를 반환하기만 하면 된다.

return ResponseEntity.ok()
			...
			.body(resource);

완성된 코드는 첨부된 MultipartController/download1, /download2 코드를 보면 확인할 수 있다.

다운로드가 정상적으로 이루어지는지 확인하려면 다음 주소에 접속한다.

http://localhost:8080/spring10/download1?filename=파일명
http://localhost:8080/spring10/download2?filename=파일명

업로드된 실제 파일명을 입력하면 다운로드가 이루어지는 것을 확인할 수 있다.

없는 파일이름을 입력하면 오류가 발생하기 때문에 조건을 걸어 Not Found(404)를 반환하도록 처리해야 한다.

  • HttpServletResponse를 이용할 경우

if(!target.exists()){
	response.sendError(404);
	return;
}
  • ResponseEntity를 이용할 경우

if(!target.exists()) {
	return ResponseEntity.notFound().build();
}

Last updated