스프링 MultipartFile 인터페이스와 파일 업로드
0. 들어가면서
Jekyll 문법과 충돌이 있기 때문에 { { someValue } }
으로 표기된 코드는 띄어쓰기를 붙여야지 정상적으로 동작한다.
1. MultipartFile 인터페이스
A representation of an uploaded file received in a multipart request.
스프링(spring) 프레임워크는 요청에 함께 전달되는 파일들을 쉽게 다룰 수 있도록 MultipartFile
인터페이스를 제공한다. 파일의 이름, 바이트 정보를 얻을 수 있고, I/O(input output)를 위한 기능들도 함께 제공한다. 이 글에선 MultipartFile 인터페이스를 사용해 파일을 업로드하는 예제를 다룬다.
MultipartFile 인터페이스의 주요 책임을 살펴보자.
- getOriginalFilename 메소드
- 클라이언트 파일 시스템에서 사용했던 파일 이름을 반환한다.
- getBytes 메소드
- 파일의 이진 바이트 값을 반환한다.
- getInputStream 메소드
- 파일을 읽기 위한 입력 스트립(stream)을 반환한다.
- transferTo 메소드
- 파일 정보를 새로운 파일 인스턴스로 복사한다.
public interface MultipartFile extends InputStreamSource {
String getName();
@Nullable
String getOriginalFilename();
@Nullable
String getContentType();
boolean isEmpty();
long getSize();
byte[] getBytes() throws IOException;
InputStream getInputStream() throws IOException;
default Resource getResource() {
return new MultipartFileResource(this);
}
void transferTo(File var1) throws IOException, IllegalStateException;
default void transferTo(Path dest) throws IOException, IllegalStateException {
FileCopyUtils.copy(this.getInputStream(), Files.newOutputStream(dest));
}
}
2. Frontend application
VueJS로 프론트엔드 애플리케이션을 구현했다. 파일을 업로드하는 컴포넌트 코드를 먼저 살펴본다. 파일 업로드에 관련된 FileUpload 컴포넌트 코드를 먼저 살펴보자. axios 모듈을 사용하여 API 요청을 수행한다.
- fetchFiles 함수
- 백엔드 서비스의
/files
경로로 파일 리스트를 요청한다.
- 백엔드 서비스의
- selectFile 함수
- 사용자가 파일 선택 완료시 호출되는 콜백 함수입니다.
FormData
인스턴스에files
이름으로 선택한 파일들을 추가한다.- 백엔드 서비스의
/files
경로로 파일 업로드를 요청한다. - 파일 업로드에 성공하면 이미지 리스트를 갱신한다.
- 파일 업로드에 실패하면 에러 메시지를 보여준다.
<template>
<div class="information">
<p>이미지를 업로드하세요.</p>
<button @click="$refs.fileRef.click">선택</button>
<input type="file" @change="selectFile" multiple accept="image/*" ref="fileRef" hidden/>
</div>
<div class="images" v-if="files.length > 0">
<div v-for="fileName in files" :key="fileName" class="image">
<img :src="`${backendUrl}/image/${fileName}`" alt="이미지">
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
data() {
return {
files: []
}
},
computed: {
backendUrl() {
return process.env.VUE_APP_BACKEND_URL
}
},
mounted() {
this.fetchFiles()
},
methods: {
async fetchFiles() {
const response = await axios.get(`${this.backendUrl}/files`)
this.files = response.data;
},
selectFile(event) {
const formData = new FormData()
for (const file of event.target.files) {
formData.append('files', file)
}
axios.post(`${this.backendUrl}/files`, formData, {
headers: {'Content-Type': 'multipart/form-data'}
}).then(() => {
this.fetchFiles()
}).catch(error => {
alert(error.message)
})
},
}
}
</script>
<style scoped>
/* styles */
</style>
3. Backend application
스프링 프레임워크로 개발한 백엔드 서버 애플리케이션을 살펴보자. 예시를 위해 생성자에서 파일 저장을 위한 디렉토리를 생성한다. 다음과 같은 엔드포인트를 만든다.
- image 메소드
- 요청 파라미터로 전달받은 이름을 가진 파일을 반환한다.
- getFileNames 메소드
- 이미지 파일 경로에 위치한 파일들의 이름을 리스트로 반환한다.
- uploadFiles 메소드
- 프론트엔드 서비스에서
FormData
인스턴스에 파일 정보를 담을 때 사용한files
라는 이름으로 파일들을 전달받는다. - 파일을 저장할 경로를 결정한다.
- 출력 스트림을 통해 업로드 된 파일의 바이트 정보를 출력한다.
- 업로드 한 파일들은 서버의 파일 시스템에 저장된다.
- 프론트엔드 서비스에서
package blog.in.action.controller;
import lombok.extern.log4j.Log4j2;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.nio.file.Paths;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Log4j2
@RestController
public class FileController {
private final static String imageDirectory = Paths.get("").toAbsolutePath() + "/images/";
public FileController() {
File file = new File(imageDirectory);
if (!file.exists()) {
file.mkdirs();
}
}
private String getExtension(MultipartFile multipartFile) {
String fileName = multipartFile.getOriginalFilename();
int index = fileName.indexOf(".");
if (index > -1) {
return fileName.substring(index);
}
return "";
}
@GetMapping("/image/{fileName}")
public ResponseEntity<Resource> image(@PathVariable String fileName) throws FileNotFoundException {
String filePath = imageDirectory + fileName;
InputStreamResource inputStreamResource = new InputStreamResource(new FileInputStream(filePath));
return ResponseEntity
.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(inputStreamResource);
}
@GetMapping("/files")
public List<String> getFileNames() {
return Stream.of(new File(imageDirectory).listFiles())
.filter(file -> !file.isDirectory())
.map(File::getName)
.collect(Collectors.toList());
}
@PostMapping(value = "/files")
public void uploadFiles(@RequestParam("files") List<MultipartFile> files) {
for (MultipartFile multipartFile : files) {
String filePath = imageDirectory + UUID.randomUUID() + getExtension(multipartFile);
try (FileOutputStream writer = new FileOutputStream(filePath)) {
writer.write(multipartFile.getBytes());
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new RuntimeException("Fail to upload files.");
}
}
}
}
4. Verify
정상적으로 동작하는지 확인해보자. 두 개의 애플리케이션을 동시에 실행해야하기 때문에 도커 컴포즈(docker compose)를 사용한다.
$ docker-compose up -d
Building frontend
[+] Building 19.5s (15/15) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 37B 0.0s
...
=> => exporting layers 0.0s
=> => writing image sha256:380f1df775b96982c1526a0d979b0198772cae31fc4fa219495cf76032f485e8 0.0s
=> => naming to docker.io/library/2021-01-20-multipartfile_backend 0.0s
WARNING: Image for service backend was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating 2021-01-20-multipartfile_frontend_1 ... done
Creating 2021-01-20-multipartfile_backend_1 ... done
애플리케이션이 모두 실행되면 브라우저에서 파일을 업로드해보자.
- 파일을 선택하여 업로드를 요청한다.
- 파일을 업로드에 성공하면 화면에 이미지 정보들이 갱신된다.

5. FileSizeLimitExceededException handling
별도 설정 없다면 높은 용량의 파일을 업로드할 때 다음과 같은 에러를 만난다.
- 톰캣(tomcat) 패키지의 LimitedInputStream 클래스에서 파일 업로드 용량 제한을 확인한다.
- 너무 큰 파일이 업로드 되는 경우
FileSizeLimitExceededException
예외를 던진다.
org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException: The field files exceeds its maximum permitted size of 1048576 bytes.
at org.apache.tomcat.util.http.fileupload.impl.FileItemStreamImpl$1.raiseError(FileItemStreamImpl.java:114) ~[tomcat-embed-core-9.0.41.jar!/:9.0.41]
at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.checkLimit(LimitedInputStream.java:76) ~[tomcat-embed-core-9.0.41.jar!/:9.0.41]
at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.read(LimitedInputStream.java:135) ~[tomcat-embed-core-9.0.41.jar!/:9.0.41]
at java.base/java.io.FilterInputStream.read(FilterInputStream.java:107) ~[na:na]
...
application.yml 파일에 용량을 늘리기 위한 설정을 추가한다.
- spring.servlet.multipart.max-file-size
- meaning total file size cannot exceed option byte.
- spring.servlet.multipart.max-request-size
- meaning total request size for a multipart/form-data cannot exceed option byte.
server:
port: 8080
spring:
servlet:
multipart:
max-file-size: 20MB
max-request-size: 20MB
댓글남기기