폴더 다운로드 기능을 구현하면서...

this-is-spear·2023년 4월 2일
0

Intro

개요

논리적인 구조로 매핑된 파일을 폴더 구조로 구성된 ZIP 파일로 반환할 필요가 있었고, (1) 논리적인 구조를 어떻게 물리적인 폴더 구조로 매핑할지, (2) ZIP 파일을 어떻게 생성하고 전달할지 고민했습니다.

목차

  • ZIP
  • ZipUtil
  • 프로젝트 구현

ZIP

찾아보려니 ZIP 과 관련된 내용이 너무 방대했고, 현재 상황에 필요한 정보들만 정리했습니다. ZIP 확장자 파일은 DEFLATE 알고리즘으로 압축된 파일을 의미합니다. DEFLATE로 압축하고 INFLATE로 해제하며 데이터를 가공합니다.

압축은 데이터의 공통 부분을 묶어 저장하는 행위를 말합니다.

ZIP은 무손실 압축 알고리즘을 이용해 데이터를 압축한다.

무손실 압축 알고리즘은 압축률이 높지 않지만 원본데이터를 유지할 필요성이 있는 곳에 많이 사용합니다. 무손실 압축 알고리즘 중 DEFLATE는 공통된 데이터를 LZ77로 압축하고 Huffman Coding을 사용해 무손실 압축을 진행합니다.

  • LZ77 방식은 반복적으로 나오는 문자열을 압축하는 방식을 말합니다.
  • Huffman Coding은 빈도 수가 높은 문자에는 짧은 이진코드를 부여하고 빈도 수가 낮은 문자는 긴 이진코드를 부여해 압축 효율을 높이는 방식을 말합니다.

ZIP은 압축률을 정해 목적에 맞게 사용할 수 있다.

Java에서는 압축 레벨은 0에서 9까지 설정할 수 있습니다.

압축 레벨에 따라 사용하는 방법을 달리할 수 있다. 압축률을 줄일 수록 CPU 부하를 줄여 빠르게 DEFLATE, INFLATE할 수 있고, 압축률을 높일 수록 데이터의 크기를 줄여 효율적으로 데이터를 교환할 수 있습니다.

압축률을 높일 수록 네트워크 자원을 줄일 수 있지만, CPU 자원을 더 많이 사용하게 됩니다. 압축률을 더 올리기 위해 불필요한 시간을 많이 사용할 필요가 없으니 압축률과 압축에 걸리는 시간을 비교해 적절한 압축 레벨을 설정할 필요가 있습니다.

ZIP 구조는 다음과 같다.

ZIP 파일은 디렉토리 구조를 CENTRAL DIRECTORY 에서 파일 엔트리를 관리해 파일들을 쉽게 관리하고 있습니다.

왜 ZIP을 사용하는지?

여러 개의 파일을 하나의 파일로 관리할 수 있다.

여러 개의 파일을 폴더 구조에 관계없이 관리가 가능하고, INFLATE 하는 경우 각 파일의 경로에 맞는 폴더 구조로 제공이 가능합니다.

데이터 크기를 줄일 수 있다.

LZ77로 압축하고 Huffman Coding을 사용해 압축 과정을 진행하면서 데이터의 크기를 줄일 수 있습니다.

ZIP64는 필요하지 않습니다.

ZIP은 최대크기는 4GB라는 한계가 있지만, 서비스정책상 ZIP 크기가 4GB를 넘길 수 없었습니다.

보안은 필요하지 않습니다.

데이터를 압축해서 제공하는 입장에서 TLS를 통해 HTTP 메시지가 암호화 되기 때문에 데이터를 암호화할 필요가 없었습니다.

ZipUtil

ZipUtil 은 개발자가 쉽게 압축 파일을 만들 수 있는 방법을 제공합니다. ZipUtil은 메서드 이름이 명확하며, 내부 동작이 직관적입니다. 반면에 복잡하지 않은 만큼 기능이 많지 않습니다.

ZipUtil 의 기능

파일 압축

예시는 다음과 같습니다.

ZipUtil.packEntry(
  new File({저장 위치} + "/{파일이름}"), 
  new File({저장하려는 위치} + "/{압축하려는 이름.zip}")
);

packEntry 는 파일만 압축할 수 있다는 제약이 있습니다. 메서드에서 알 수 있듯이 Entry는 압축 유틸에서는 파일로 취급하고 있습니다.

/**
 * Compresses the given file into a ZIP file.
 * The ZIP file must not be a directory and its parent directory must exist.
 *
 * @param fileToPack  file that needs to be zipped.
 * @param destZipFile ZIP file that will be created or overwritten.
 */
public static void packEntry(File fileToPack, File destZipFile) {
  packEntry(fileToPack, destZipFile, IdentityNameMapper.INSTANCE);
}

그럼 쉽게 폴더 압축 메서드는 특정할 수가 있습니다.

폴더 압축

예시는 다음과 같습니다.

  ZipUtil.pack(new File("저장된 폴더 위치"), new File("{저장할 위치}" + "/{저장할 압축 파일}.zip"));

pack은 폴더를 압축하는 메서드입니다. 폴더 압축은 쉽게 구현할 수 있습니다.

/**
 * Compresses the given directory and all its sub-directories into a ZIP file.
 * The ZIP file must not be a directory and its parent directory must exist.
 * Will not include the root directory name in the archive.
 *
 * @param rootDir root directory.
 * @param zip.    ZIP file that will be created or overwritten.
 */
public static void pack(File rootDir, File zip) {
  pack(rootDir, zip, DEFAULT_COMPRESSION_LEVEL);
}

ZipUtil에는 오버로딩을 통해 다형성을 만족하고 있습니다. 그 중 제가 필요한 API를 하나 소개하겠습니다.

상위 폴더 추가 후 추가된 압축 폴더 및 기존 압축 폴더 추가

예시는 다음과 같습니다.

File 압축되어_있는_파일 = new File({저장 위치} + "/{압축한 이름.zip}");
ZipUtil.pack(
  new File(압축할_디렉토리), 
  압축되어_있는_파일, 
  name -> "{추가할 상위폴더}/" + name
);

해당 API를 활용하면 기존에 압축되어 있던 폴더와 추가한 폴더를 상위 폴더에 추가할 수 있습니다.

/**
 * Compresses the given directory and all its sub-directories into a ZIP file.
 * The ZIP file must not be a directory and its parent directory must exist.
 *
 * @param sourceDir root directory.
 * @param targetZip ZIP file that will be created or overwritten.
 * @param mapper    call-back for renaming the entries.
 */
public static void pack(File sourceDir, File targetZip, NameMapper mapper) {
  pack(sourceDir, targetZip, mapper, DEFAULT_COMPRESSION_LEVEL);
}

여기서 더욱 활용할 수 있는 부분은 특정 조건을 만족하는 경우만 폴더에 추가할 수 있는 방식으로 구현할 수 있습니다. 아래 예시를 작성했습니다.

ZipUtil.pack(new File(Path), 기존_압축_파일, name -> {
    if ("{특정 조건을 만족한다면 추가해라}") {
        return "foo/" + name;
    }
    return name;
});

압축 파일에 파일 리스트 추가

예시는 다음과 같습니다.

File 기존_압축_파일 = new File("{저장된_위치}" + "/{저장된 압출파일}.zip");
ZipEntrySource[] 저장할_파일들 = new ZipEntrySource[] {
    new FileSource("저장할 위치" + "/{저장할 이름}", new File("저장된 파일 위치" + "/{저장된 파일 이름}")),
    new FileSource("저장할 위치" + "/{저장할 이름}", new File("저장된 파일 위치" + "/{저장된 파일 이름}")),
    new FileSource("저장할 위치" + "/{저장할 이름}", new File("저장된 파일 위치" + "/{저장된 파일 이름}")),
};
ZipUtil.addEntries(기존_압축_파일, 저장할_파일들);

addEntries는 기존 압축 파일에 파일들을 추가할 수 있습니다. 다른 기능과 마찬가지로 오버로딩을 활용해 여러 기능 들을 제공하고 있습니다.

/**
 * Changes a zip file it with with new entries. in-place.
 *
 * @param zip     an existing ZIP file (only read).
 * @param entries new ZIP entries appended.
 */
public static void addEntries(final File zip, final ZipEntrySource[] entries) {
  operateInPlace(zip, new InPlaceAction() {
    public boolean act(File tmpFile) {
      addEntries(zip, entries, tmpFile);
      return true;
    }
  });
}

제가 구현한 프로젝트에서 파일 단위로 관리하지 않고, 논리적인 단위로 폴더를 구현하기 때문에 데이터베이스 정보를 이용해 폴더 압축을 구현해야 합니다. ZipUtil 기능만으로 쉽게 폴더 압축을 구현할 수 있을 거라 판단했고, 프로젝트에 적용했습니다.

프로젝트에 적용

구현 과정 설명

폴더 다운로드 구현은 완전 탐색 알고리즘과 함께 ZipUtil.addEntries 메서드를 활용했습니다.

private void createZipFile(String folderId, Path path, MyFolder ensureFolder) {
    File zip = new File(path.resolve(folderId) + ".zip");
    ZipUtil.createEmpty(zip);

    // 1. 압축 파일에 들어갈 데이터 조회
    var files = findFilesRecursive("", myFolder).toArray(new ZipEntrySource[] {});
    ZipUtil.addEntries(zip, files);
}

private List<ZipEntrySource> findFilesRecursive(String path, MyFolder myFolder) {
    var list = new ArrayList<>();
    // 2. 현재 위치의 파일을 리스트에 추가
    저장소.파일_조회_요청(myFolder.getId())
        .map(myFile -> list.add(new FileSource(resolvePath(path, myFile.getName()),
            new File(resolvePath(myFile.getPath(), myFile.getId())))));
    // 3. 하위 폴더 탐색
    저장소.폴더_조회_요청(myFolder.getId())
        .map(nextFolder -> list.addAll(findFilesRecursive(resolvePath(path, myFolder.getName()), nextFolder)));
    return list;
}

신경써야 할 부분

  • 압축률과 압축에 걸리는 시간을 비교해 적절한 압축 레벨을 설정할 필요가 있습니다.
  • ZIP을 만드는 여러 개의 요청이 들어올 때, 다른 ZIP에 영향을 끼치지 않아야 합니다.
  • ZIP을 만든 다음 응답했을 때, 저장된 ZIP 데이터를 어떻게 정리할지 고민해야 합니다.

마지막

정리하자면

  • ZIP 구조를 이해할 수 있었습니다.
  • ZipUtil을 이용해 문제를 해결할 수 있었습니다.
  • ZIP을 활용할 때 신경써야 할 부분이 있었습니다.

참고자료

https://github.com/zeroturnaround/zt-zip

https://www.rfc-editor.org/rfc/rfc1951

https://www.rfc-editor.org/rfc/rfc1952

profile
익숙함을 경계하자

0개의 댓글