현재 진행중인 그룹웨어 개발 프로젝트에서는 게시판, 기안문 등 다양한 곳에서 파일 다운로드가 가능해야 한다.
그래서 중복 코드를 방지하기 위해 파일 다운로드를 위한 코드를 작성해 util 패키지에 넣어두었다!
아래는 개발된 코드이다.
/* 파일 다운로드 */
/*
* ori_filename : 파일의 원래 이름
* new_filename : UUID
*/
function download(ori_filename, new_filename) {
console.log(ori_filename, new_filename);
$.ajax({
type: 'GET',
url: '/download',
data: {
'ori_filename': ori_filename,
'new_filename': new_filename
},
xhrFields: {
responseType: 'blob' // 서버 응답을 Blob 형태로 처리
},
success: function(blob) {
// 파일 다운로드 처리
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = ori_filename; // 다운로드 파일 이름 설정
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
},
error: function(e) {
console.error("다운로드 실패:", e);
}
});
}
파일 다운로드를 위해 UUID로 생성된 파일 이름과 사용자가 다운로드 시 보여줄 이름인 원 파일명을 파라미터로 넘겨준다.
xhrFields 추가해 서버 응답을 blob 형태로 처리한다.
서버에서 받은 blob 데이터를 URL 객체로 변환한 후, <a> 태그를 생성하여 다운로드 링크를 실행합니다.
이진 데이터 저장 : 텍스트, 바이너리 파일, 이미지 등의 데이터를 저장할 수 있고, 파일 시스템에 저장된 데이터를 메모리로 불러오거나 브라우저에서 동작하는 데이터를 처리하는데 사용한다.
데이터는 파일처럼 취급 되지만 메모리에서 작업된다. 파일이름, MIME 타입 등 메타데이터를 포함 해 다룰 수 있다.
서버 처리 코드는 아래를 참고하면 된다.
@GetMapping(value = "/download")
public ResponseEntity<Resource> download(@RequestParam String ori_filename, @RequestParam String new_filename) {
try {
// 파일 경로 생성
// 1. 서버에 저장된 파일 경로 생성
// Paths.get(paths) : 설정된 업로드 경로 (paths 변수)를 기반으로 Path 객체를 생성.
// .resolve(new_filename) : 업로드 경로에 전달된 파일 이름(new_filename)을 결합하여 전체 경로를 만듦.
// .normalize() : 경로를 표준 형식으로 정리 (불필요한 ".." 등 제거).
Path filePath = Paths.get(paths).resolve(new_filename).normalize();
Resource resource = new UrlResource(filePath.toUri());
if (!resource.exists() || !resource.isReadable()) {
throw new RuntimeException("파일을 찾을 수 없거나 읽을 수 없습니다: " + new_filename);
}
// 한글 파일 이름 URL 인코딩
String encodedFilename = URLEncoder.encode(ori_filename, StandardCharsets.UTF_8.toString())
.replace("+", "%20"); // 공백 처리
// 파일 다운로드 응답 생성
// 1. HTTP 응답 생성 (200 OK 상태)
return ResponseEntity.ok()
// 2. 응답의 Content-Type 설정
// MediaType.APPLICATION_OCTET_STREAM : 바이너리 데이터(예: 파일)를 나타내는 MIME 타입.
.contentType(MediaType.APPLICATION_OCTET_STREAM)
// 3. Content-Disposition 헤더 추가
// "attachment" : 파일을 브라우저에서 다운로드하도록 지시.
// filename*=UTF-8'' + encodedFilename : RFC 5987 형식으로 UTF-8로 인코딩된 파일 이름 설정.
// encodedFilename : 파일 이름을 URLEncoder를 사용해 UTF-8로 인코딩한 값.
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFilename)
// 4. 응답 본문에 파일 데이터를 포함
// resource : UrlResource 객체를 통해 파일 내용을 HTTP 응답의 본문으로 포함.
.body(resource);
} catch (Exception e) {
logger.error("파일 다운로드 중 문제가 발생했습니다.", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
❗ 문제 상황
다운로드는 완료되지만 java.lang.IllegalArgumentException: code point [51,228]에 위치한 유니코드 문자 [제]은(는), 0에서 255까지의 허용 범위 바깥에 있으므로 인코딩될 수 없습니다 이런 에러가 발생했다.
찾아보니 한글 파일의 경우 Content-Disposition 헤더는 기본적으로 ASCII 문자만 허용하며, 한글과 같은 유니코드 문자는 직접 포함할 수 없기 때문에 한글 파일 이름을 사용하려면 URL 인코딩 또는 RFC 5987 형식으로 처리해줘야 하는 것을 알게되었다.
그래서 최종적으로 String encodedFilename = URLEncoder.encode(ori_filename, StandardCharsets.UTF_8.toString()).replace("+", "%20"); 코드를 사용해 문제를 해결!!
코드 설명
URLEncoder.encode: 파일 이름을 UTF-8로 인코딩하여 HTTP 헤더에 넣을 수 있는 ASCII 문자로 변환
filename*=UTF-8'': RFC 5987 규격에 따라 인코딩된 파일 이름을 브라우저가 처리할 수 있도록 설정
공백 처리: URLEncoder는 공백을 +로 인코딩하므로 이를 %20으로 변경
이렇게 해서 최종적으로 파일 다운로드를 완료했다 👍
그렇다면 이제 Util class 로 기능을 분리해서 프로젝트원들이 사용할 수 있게 만들어 줘야 한다!
공통 코드로 사용하기 위해 static 으로 선언해줘야 한다. ❗❗
public static ResponseEntity<Resource> download(String paths, String ori_filename, String new_filename) {
try {
// 파일 경로 생성
Path filePath = Paths.get(paths).resolve(new_filename).normalize();
Resource resource = new UrlResource(filePath.toUri());
if (!resource.exists() || !resource.isReadable()) {
throw new RuntimeException("파일을 찾을 수 없거나 읽을 수 없습니다: " + new_filename);
}
// 한글 파일 이름 URL 인코딩
String encodedFilename = URLEncoder.encode(ori_filename, StandardCharsets.UTF_8.toString())
.replace("+", "%20"); // 공백 처리
// 파일 다운로드 응답 생성
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFilename)
.body(resource);
} catch (Exception e) {
throw new RuntimeException("파일 다운로드 중 문제가 발생했습니다.", e);
}
}
사용할 때에는 정적 메서드를 호출해 사용하면 된다!
@GetMapping("/download")
public ResponseEntity<Resource> downloadFile(@RequestParam String ori_filename, @RequestParam String new_filename) {
// FileUtil의 정적 메서드 호출
return CommonUtil.download(paths, ori_filename, new_filename);
}