[그룹웨어] 공통 기능 만들기 (파일 다운로드)

yihyun·2024년 12월 13일

ERP 개발 프로젝트

목록 보기
6/11

현재 진행중인 그룹웨어 개발 프로젝트에서는 게시판, 기안문 등 다양한 곳에서 파일 다운로드가 가능해야 한다.

그래서 중복 코드를 방지하기 위해 파일 다운로드를 위한 코드를 작성해 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> 태그를 생성하여 다운로드 링크를 실행합니다.

Blob 객체

  • 이진 데이터 저장 : 텍스트, 바이너리 파일, 이미지 등의 데이터를 저장할 수 있고, 파일 시스템에 저장된 데이터를 메모리로 불러오거나 브라우저에서 동작하는 데이터를 처리하는데 사용한다.

  • 데이터는 파일처럼 취급 되지만 메모리에서 작업된다. 파일이름, 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 로 기능을 분리해서 프로젝트원들이 사용할 수 있게 만들어 줘야 한다!

util 클래스로 만들기

공통 코드로 사용하기 위해 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);
    }
profile
개발자가 되어보자

0개의 댓글