파일 다운로드를 하는 방법은 여러가지가 있는데,
가장 간단하게는
location.href = 'api 주소';
로 구현할 수 있다.
제이쿼리를 쓰고 있는 프로젝트였고, 파일의 크기가 클 수 있기 때문에
사용자들이 기다리는 것을 위해서 spinner 를 다운로드 받는 중에 노출 시키도록 변경 하려고 했다.
단순히 링크만 보내는 것은 파일이 다운로드가 완료 된 후에 spinner를 끄는 것이 불가하기 때문에
ajax를 사용하기로 한다.
일반적으로 Ajax 요청은 JSON,XML,HTML과 같은 텍스트 데이터를 처리하지만, 파일 다운로드의 경우 바이너리데이터(blob)으로 응답이 전달된다.
그렇기 때문에
xhrFields: {
responseType: 'blob'
}
와 같은 설정이 필요하다.
$.ajax({
url: /download?${filePaths},
method: 'GET',
xhrFields: {
responseType: 'blob',
withCredentials: true // 쿠키를 포함하여 요청
},
success: function(data, status, xhr) {
// 파일 이름 추출 (백엔드의 Content-Disposition 헤더 사용)
var filename = "";
var disposition = xhr.getResponseHeader('Content-Disposition');
if (disposition && disposition.indexOf('attachment') !== -1) {
var matches = /filename="([^"]*)"/.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1];
}
}
// Blob을 사용해 파일 다운로드
var blob = data; // responseType이 blob일 때, data가 이미 blob으로 전달됨
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename || 'download.zip'; // 기본 파일 이름 설정
link.click();
},
error: function() {
alert("File download failed.");
}
});
},
error: function(xhr, status, error) {
console.error('Error:', error);
}
})
});
위와 같은 코드를 작성하였는데,
Uncaught InvalidStateError: Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text' (was 'blob'). at XMLHttpRequest.c (jquery.min:3:26787)
에러발생..
찾아보니 jquery 버전의 문제였다.
jquery 1.12.1 버전을 3.3.1버전으로 변경하게 되면 문제가 발생하지 않는다고 한다.
하지만, 나 혼자 하는 프로젝트가 아니다 보니
jquery 버전을 올릴 수는 없고,
fetch방식으로 변경하게 되었다.
fetch('/download')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.blob(); // Blob으로 응답을 처리
})
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'yourfile.zip'; // 원하는 파일 이름 지정
document.body.appendChild(a);
a.click();
a.remove();
})
.catch(error => console.error('There has been a problem with your fetch operation:', error));
fetch 방식으로 변경하다 보니 그래도 에러가 계속 발생함
디버깅을 해보니, 서버측에서 발생하는 오류로
Could not write content: No serializer found for class java.io.FileDescriptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: org.springframework.core.io.InputStreamResource["inputStream"]->java.io.FileInputStream["fd"])
이 오류는 Spring이 InputStreamResource에서 InputStream을 직렬화 하려고 시도하면서 발생한 오류이다.
Spring이 직렬화할 수 없는 객체를 포함하고 있어서 나타나는 문제다..
왜 href.location 방식에서는 나타나지 않던 문제가 fetch API에서는 발생한건가?
- fetch를 사용하는 경우 자바스크립트로 HTTP 요청을 보내고, 서버로부터 받은 응답을 수동으로 처리해야한다.
- fetch는 JSON응답을 처리하기 위해 설계된 API로 기본적으로 JSON으로 변환하려고 시도한다.
- 이 과정에서 InputStreamResource 같은 스트리밍 데이터를 처리할 때 문제가 발생할 수 있다.
반면, href를 동해 하게 된다면, 응답에 Content-Disposition 헤더가 포함되어 있어 브라우저가 파일을 다운로드를 처리할 수 있다.
수정된 서버 코드
public void downloadZip(HttpServletResponse response) {
Path zipFilePath;
try {
// Zip 파일 생성
zipFilePath = fileService.createZipFile("filepath");
// 파일이 존재하는지 확인
if (!Files.exists(zipFilePath)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Zip file not found: " + zipFilePath.toString());
return;
}
File zipFile = zipFilePath.toFile();
try (FileInputStream fileInputStream = new FileInputStream(zipFile);
OutputStream outputStream = response.getOutputStream()) {
// 응답 헤더 설정
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + new String(zipFile.getName().getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1) + "\"");
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
// 파일 내용을 스트리밍
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fileInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
} catch (IOException e) {
// IO 예외 처리
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to download zip file: " + e.getMessage());
} catch (Exception e) {
// 일반 예외 처리
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "An unexpected error occurred: " + e.getMessage());
}
}
}
클라이언트로 파일 전송을 위해 HttpServletResponse를 사용하여 응답을 직접 설정했다.
원래 문제가 발생한 이유
- InputStreamResource로 zip 파일을 응답으로 반환할 때, Spring이 JSON으로 직렬화 하려고 시도함
- InputStreamResource내부에서 사용되는 FileInputStream은 직렬화 할 수 없는 클래스
- 그 과정에서 오류가 발생
수정된 원리
- HttpServletResponse를 사용하여 직접 응답을 구성하여 Spring의 JSON 직렬화 하는 것을 우회할 수 있다.
- 응답을 직접 설정하므로 파일을 스트리밍 할 수 있고, 이 과정에서 Spring은 JSON으로 직렬화 하려고 시도하지 않는다.
- FileInputStream을 사용하여 Zip파일 내용을 읽고, OutputStream을 통해 클라이언트에 직접 전송한다. 메모리 사용이 효율적이다.
- try-with-resources 를 사용하여 자동으로 자원을 닫아 메모리 누수를 방지한다.
'Backend · Infra' 카테고리의 다른 글
Docker 인증서 오류(x509) 해결기: SSL 인증서와의 전쟁 (0) | 2025.02.07 |
---|---|
WSL 설치부터 Docker CLI 설치까지 완벽 가이드 🐳 (0) | 2025.02.06 |
rocky 9 linux 초기 셋팅 (1) | 2024.10.17 |
[Windows] 특정 포트 사용 프로세스 종료하기 (2) | 2024.08.08 |
[Error] No SLF4J providers were found. (0) | 2024.08.06 |