[SpringBoot] 파일 다운로드 구현
File Download Method
웹개발을 하면서 제일 자주 잊어먹고 개발 할때마다 reference를 찾아서 하는 것이 파일 다운로드였다.
막상 개발해보면 로직이 어렵지 않지만 어떤 모듈을 써서 개발했는지 헷갈려서 매번 다시 찾아서 했었다.
그렇게 개발하다보니 몇가지 다운로드 구현 방식의 패턴이 보였고
직접 사용해 보지 못한 방식까지 찾아서 같이 포스팅한다.
방법 1. HttpServletResponse ( return Type : Void )
직접 구현해본 다운로드 기능 중 가장 많은 비중을 차지 한 방식이다.
이 방식은 Spring Framwork를 사용해보기전 Pure Java로 처음 Java 개발할 때부터 사용한 방식이라 자주 사용한 것 같다.
<예시 코드>
@Controller
public class FileController {
@Value("${file.root}") // yml에서 경로 받아오기
private String fileRoot;
@GetMapping("/file/downloadFile/{fileName}")
public void downloadRagFile(@PathVariable String fileName, HttpServletResponse response) {
// 파일 경로 조회
FileModel fileModel = fileService.getFile(fileName);
if (fileModel == null) {
throw new HttpClientErrorException(HttpStatus.NOT_FOUND, "파일을 찾을 수 없습니다.");
}
// 파일이 위치한 경로 + 저장된 파일명 + 확장자
File file = new File(fileRoot + "/" + fileModel.getFilePath() + "/" + fileModel.getFileSaveName() + "." + fileModel.getFileFormat());
if (!file.exists()) { // 디렉토리에 파일 없을시
throw new HttpClientErrorException(HttpStatus.NOT_FOUND, "파일을 찾을 수 없습니다.");
}
response.setContentType("application/octet-stream");
String encodedFileName = "";
try { // 한글명 깨지지 않도록 인코딩
encodedFileName = URLEncoder.encode(fileModel.getFileName(), "UTF-8").replaceAll("\\+", "%20");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("파일 이름 인코딩 중 오류가 발생했습니다.", e);
}
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName); // 사용자가 다운받을 시 보이는 이름
response.setContentLength((int) file.length());
try (InputStream inputStream = new FileInputStream(file);
OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
} catch (IOException ex) {
throw new RuntimeException("파일 다운로드 중 오류가 발생했습니다.", ex);
}
}
}
<특징>
- 가장 기본적인 방법
- 직접 스트림을 다루기 때문에 세세한 제어가 가능
방법 2. Resource ( return Type : ResponseEntity< Resource > )
이 방법은 최근 들어서 자주 쓰는 방식이다.
위 HttpServletResponse를 쓸 때와 비교해서 코드가 훨씬 간결해진다는 것을 볼수 있다.
<예시코드>
@RestController
public class FileDownloadController {
@GetMapping("/download")
public ResponseEntity<Resource> downloadFile(@RequestParam String fileName) {
File file = new File("path/to/files/" + fileName);
if (!file.exists()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
Resource resource = new FileSystemResource(file);
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\"");
headers.add(HttpHeaders.CONTENT_TYPE, "application/octet-stream");
return ResponseEntity.ok()
.headers(headers)
.body(resource);
}
}
<특징>
- 스프링의 기능을 잘 활용한 방식 ( 스프링 사용시에만 가능 )
- 코드가 더 깔끔하고 유지보수가 쉬움
방법 3. StreamingResponseBody
( return Type :ResponseEntity<StreamingResponseBody>)
이 방식은 직접 사용해 본적은 없지만 다른 파일 다운로드 구현법을 찾다가 발견했다.
기존에 개발해본 일반적인 문서 파일의 다운로드와는 다르게
고용량의 파일을 다운로드 할때 쓰이는 방법이다.
<예시코드>
@RestController
public class FileDownloadController {
@GetMapping("/download")
public ResponseEntity<StreamingResponseBody> downloadFile(@RequestParam String fileName) {
File file = new File("path/to/files/" + fileName);
if (!file.exists()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
StreamingResponseBody responseBody = outputStream -> {
try (InputStream inputStream = new FileInputStream(file)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
};
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(responseBody);
}
}
<특징>
- 대용량 파일 전송에 적합
- 스트리밍 방식으로 데이터를 전송가능
방법 4. Spring WebFlux ( return Type : Mono<ResponseEntity< Resource > >)
Spring에서 제공하는 비동기, 논블로킹 방식을 지원하는 프레임워크, WebFlux를 활용한 방법이다.
한개의 파일 대상으로하는 다운로드 로직이기 때문에
단일 개체의 비동기 값을 처리하는 Mono 타입을 사용했다.
** Mono : 0 혹은 1개의 비동기 값을 처리하는 Reactor 타입
** Flux : 0 또는 N개의 비동기 값을 처리하는 Reactor 타입
<예시코드>
@RestController
public class FileDownloadController {
@GetMapping("/download")
public Mono<ResponseEntity<Resource>> downloadFile(@RequestParam String fileName) {
File file = new File("path/to/files/" + fileName);
if (!file.exists()) {
return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).build());
}
Resource resource = new FileSystemResource(file);
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\"");
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
return Mono.just(ResponseEntity.ok()
.headers(headers)
.body(resource));
}
}
<특징>
- 비동기 및 논블로킹 방식으로 파일 전송가능