๐Ÿ’ Spring

[Spring MVC] Zip, UnZip ์—…๋กœ๋“œ ๋ฐ ๋‹ค์šด๋กœ๋“œ API ๊ตฌํ˜„ (feat. MultipartFile)

iseunghan 2024. 6. 22. 20:37
๋ฐ˜์‘ํ˜•

 

๊ฐœ๋ฐœํ™˜๊ฒฝ

๐Ÿ’ก ์ „์ฒด ์ฝ”๋“œ๋Š” Github๋ฅผ ์ฐธ์กฐํ•ด์ฃผ์„ธ์š”.
  • spring boot 3.3.1
  • JDK 17

Intro


Spring MVC์—์„œ๋Š” MultipartFile ์ด๋ผ๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด ํŒŒ์ผ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ZIP, UnZIP ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ์œ ํ‹ธ์„ฑ ํด๋ž˜์Šค๋ฅผ ํ•จ๊ป˜ ๊ฐœ๋ฐœํ•ด๋ณด๊ณ , MultipartFile์„ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ ํŒŒ์ผ ์—…๋กœ๋“œ, ๊ทธ๋ฆฌ๊ณ  HttpServletResponse๋ฅผ ํ†ตํ•ด ํŒŒ์ผ์„ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ŠคํŠธ๋ฆผ์„ ํ†ตํ•ด ๋‚ด๋ ค์ค˜์„œ ๋‹ค์šด๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋Š” API๋ฅผ ๊ฐœ๋ฐœ ๋ฐ ํ…Œ์ŠคํŠธ ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ๋ฐฐ์›Œ๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

๋จผ์ € Multipartfile์ด ๋ญ˜๊นŒ์š”?


๋ณดํ†ต ํŒŒ์ผ์„ ์ „์†กํ•  ๋•Œ, HTTP body๋ฅผ ์—ฌ๋Ÿฌ ๋ถ€๋ถ„(Multipart Data)์œผ๋กœ ๋‚˜๋ˆ ์„œ ๋ณด๋‚ด๋Š”๋ฐ ์ด๋Ÿฌํ•œ Multipart Data ์ฆ‰, Multipart ์š”์ฒญ์œผ๋กœ ๋ฐ›์€ ์—…๋กœ๋“œ๋œ ํŒŒ์ผ์„ ๋งํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด ํŒŒ์ผ์˜ ์ •๋ณด(์‚ฌ์ด์ฆˆ, ํŒŒ์ผ ์ด๋ฆ„ ๋“ฑ)๋ฅผ ์‰ฝ๊ฒŒ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์Šคํ”„๋ง์—์„œ๋Š” ์š”์ฒญ์œผ๋กœ ๋ฐ›์€ ํŒŒ์ผ ์ •๋ณด๋ฅผ ๋ฉ”๋ชจ๋ฆฌ ๋˜๋Š” ๋””์Šคํฌ์— ์ผ์‹œ์ ์œผ๋กœ ์ €์žฅ์‹œ์ผœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค. ์ž„์‹œ ๋ฐ์ดํ„ฐ๋Š” ์š”์ฒญ ์ฒ˜๋ฆฌ๊ฐ€ ๋๋‚  ๋•Œ ์ •๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

1. Zip ๊ตฌํ˜„


๋จผ์ € ZIP ๊ธฐ๋Šฅ์„ ๋จผ์ € ๊ตฌํ˜„ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. API๋ฅผ ์–ด๋–ป๊ฒŒ ์„ค๊ณ„ํ•˜๋ƒ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง€๊ฒ ์ง€๋งŒ, ์›ํ•˜๋Š” ๋””๋ ‰ํ† ๋ฆฌ ๋˜๋Š” ํŒŒ์ผ์„ ์ œ๊ณต๋ฐ›๊ณ  ํ•ด๋‹น ๋””๋ ‰ํ† ๋ฆฌ ๋˜๋Š” ํŒŒ์ผ์„ Zippingํ•˜๋Š” ๊ฐ„๋‹จํ•œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

public static byte[] zipFile(File sourceFile) {
    try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
        try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) {
            addFileToZip(sourceFile, zipOutputStream, null);
        }
        return byteArrayOutputStream.toByteArray();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

๋จผ์ € ZIP์œผ๋กœ ์“ฐ๊ธฐ ์œ„ํ•ด์„œ๋Š” ํŒŒ์ผ ๋‚ด๋ถ€์— byte๋กœ ์ž‘์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ByteArrayOutputStream๊ณผ ZipOutputStream์„ try-with-resource๋ฅผ ํ†ตํ•ด ์—ด์–ด์ค๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ ๋Š” addFileToZip ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋Š”๋ฐ์š” ํ•ต์‹ฌ ๋กœ์ง์€ ํ•ด๋‹น ๋ฉ”์„œ๋“œ ๋‚ด๋ถ€์— ์žˆ์Šต๋‹ˆ๋‹ค.

private static void addFileToZip(File parentFile, ZipOutputStream zipOutputStream, String parentFolderName) {
    String currentPath = hasText(parentFolderName) ? parentFolderName + "/" + parentFile.getName() : parentFile.getName();
    if (parentFile.isDirectory()) {
        for (File f : Objects.requireNonNull(parentFile.listFiles())) {
            addFileToZip(f, zipOutputStream, currentPath);
        }
    } else {
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        try (FileInputStream fileInputStream = new FileInputStream(parentFile)) {
            zipOutputStream.putNextEntry(new ZipEntry(currentPath));

            int length;
            while ((length = fileInputStream.read(buffer)) > 0) {
                zipOutputStream.write(buffer, 0, length);
            }
            zipOutputStream.closeEntry();
        } catch (FileNotFoundException e) {
            log.error("File not found: {}, Exception: {}", parentFile, e.getMessage());
            throw new RuntimeException(e);
        } catch (IOException e) {
            log.error("Error while zipping file: {}, Exception: {}", parentFile, e.getMessage());
            throw new RuntimeException(e);
        }
    }
}

ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋Š” ์žฌ๊ท€ ํ•จ์ˆ˜์ฒ˜๋Ÿผ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ZipOutputStream์„ ์ „๋‹ฌ๋ฐ›์•„์„œ ํ˜„์žฌ ๊ฒฝ๋กœ๋ฅผ ๋จผ์ € ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. (ํด๋”๊ฐ€ ์—ฌ๋Ÿฌ depth๋กœ ๊ตฌ์„ฑ๋˜์–ด์žˆ๋Š” ๊ฒƒ์„ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด)

ํ˜„์žฌ ํŒŒ์ผ์ด ๋””๋ ‰ํ† ๋ฆฌ๋ผ๋ฉด? ์žฌ๊ท€ ํ˜ธ์ถœ์„ ํ•˜๊ณ , ํŒŒ์ผ์ด๋ผ๋ฉด? ํŒŒ์ผ ์ŠคํŠธ๋ฆผ์„ ์—ด์–ด์„œ ZipEntry์— write ํ•ด์ค๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ์ฃผ์˜ํ•  ์ ์€ ๋‚ด๋ถ€๊ฐ€ ๋น„์–ด์žˆ๋‹ค๋ฉด fileInputStream.read(buffer) ์ด ๋ถ€๋ถ„์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ๋ถ€๋ถ„์€ try-catch๋ฅผ ํ†ตํ•ด ์žก์•„์ฃผ์‹œ๋˜์ง€ ์ง€๊ธˆ์ฒ˜๋Ÿผ ๊ทธ๋ƒฅ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒํ•˜๋˜์ง€ ํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

1.1. Zip ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ


@Test
void directoryZippingTest() throws IOException {
    // given
    final Path zipFilePath = Path.of("src/test/resources/test-dir");
    final Path testZipPath = Path.of("src/test/resources/test.zip");
    Files.deleteIfExists(testZipPath);  // delete test-zip

    // when
    byte[] bytes = ZipUtils.zipFile(zipFilePath.toFile());
    Files.write(testZipPath, bytes);

    // then
    assertThat(testZipPath).exists();
}
@Test
void fileZippingTest() throws IOException {
    // given
    final Path zipFilePath = Path.of("src/test/resources/ํ•œ๊ธ€์ด๋ฆ„_ํ…Œ์ŠคํŠธ.txt");
    final Path testZipPath = Path.of("src/test/resources/test2.zip");
    Files.deleteIfExists(testZipPath);  // delete test-zip

    // when
    byte[] bytes = ZipUtils.zipFile(zipFilePath.toFile());
    Files.write(testZipPath, bytes);

    // then
    assertThat(testZipPath).exists();
}

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. Zipping ํ•  ํŒŒ์ผ ๋˜๋Š” ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋„˜๊ฒจ์„œ byte[]๋ฅผ ๋ฐ˜ํ™˜๋ฐ›์€ ๋‹ค์Œ, ํ•ด๋‹น byte[]๋ฅผ ํŒŒ์ผ๋กœ ์จ์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

2. unZip ๊ธฐ๋Šฅ ๊ตฌํ˜„


unZip ๊ธฐ๋Šฅ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. Zip ํŒŒ์ผ(zipFile) ๋˜๋Š” InputStream์„ ์••์ถ• ํ•ด์ œํ•  ๋””๋ ‰ํ† ๋ฆฌ(dstPath)๋กœ ์ž…๋ ฅ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

public static void unZipFile(String dstPath, File zipFile) throws IOException {
    unZipFile(dstPath, new FileInputStream(zipFile), null);
}

public static void unZipFile(String dstPath, InputStream inputStream) throws IOException {
    unZipFile(dstPath, inputStream, null);
}

๊ฒฐ๋ก ์ ์œผ๋ก  InputStream์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ํ•ต์‹ฌ ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฉ”์„œ๋“œ๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

private static void unZipFile(String dstPath, InputStream inputStream, @Nullable Function<Path, Path> renameDuplicatedFilename) throws IOException {
  try (ZipInputStream zipInputStream = new ZipInputStream(inputStream, Charset.forName("EUC-KR"))) {
      ZipEntry entry;
      byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];

      while ((entry = zipInputStream.getNextEntry()) != null) {
          final String entryName = entry.getName();
          if (entry.isDirectory() || entryName.startsWith("__MACOSX")) continue;

          File entryFile;
          if (renameDuplicatedFilename == null) {
              entryFile = new File(dstPath, entryName);
          } else {
              Path path = Path.of(dstPath, entryName);
              entryFile = renameDuplicatedFilename.apply(path).toFile();
          }
          Files.createDirectories(entryFile.getParentFile().toPath());

          try (BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(entryFile))) {
              int bytesRead;
              while ((bytesRead = zipInputStream.read(buffer)) != -1) {
                  outputStream.write(buffer, 0, bytesRead);
              }
          }
          zipInputStream.closeEntry();
      }
  }
}

์ „๋‹ฌ ๋ฐ›์€ inputStream์„ ์ด์šฉํ•ด์„œ ZipInputStream์„ ์—ด์–ด์ค๋‹ˆ๋‹ค. ๊ฐ entry๋ฅผ ์ˆœํšŒํ•˜๋ฉด์„œ Directory๊ฑฐ๋‚˜ __MACOSX๋กœ ์‹œ์ž‘ํ•œ๋‹ค๋ฉด ๊ฑด๋„ˆ๋„์–ด์ค๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•œ ์ด์œ ๋Š” ๋””๋ ‰ํ† ๋ฆฌ์— ์†ํ•œ ์—”ํŠธ๋ฆฌ๋ฅผ ์˜ฎ๊ธธ ๋•Œ ์†ํ•œ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑ ์‹œํ‚ฌ ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ํ˜น์‹œ๋ผ๋„ ์••์ถ•์„ ํ•ด์ œํ•˜๋Š” ๊ณณ์— ๋™์ผํ•œ ์ด๋ฆ„์˜ ํŒŒ์ผ์ด ์กด์žฌํ•œ๋‹ค๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋‹ˆ renameDuplicatedFilename๋ฅผ ์ด์šฉํ•ด์„œ rename์„ ํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

2.1. UnZip ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ


@Test
void unzipTest() throws IOException {
    // given
    Path dstPath = Path.of("src/test/resources/unzip-result");
    FileUtils.deleteDirectory(dstPath.toFile());
    final Path testZipPath = Path.of("src/test/resources/test.zip");

    // when
    ZipUtils.unZipFile(dstPath.toString(), testZipPath.toFile());

    // then
    assertThat(dstPath).exists();
    assertThat(Path.of(dstPath.toString(), "test-dir", "ํ•œ๊ธ€์ด๋ฆ„_ํ…Œ์ŠคํŠธ.txt")).exists();
    assertThat(Path.of(dstPath.toString(), "test-dir", "file2.txt")).exists();
    assertThat(Path.of(dstPath.toString(), "test-dir", "file3.txt")).exists();
    assertThat(Path.of(dstPath.toString(), "test-dir", "test-dir2", "ํ•œ๊ธ€์ด๋ฆ„_ํ…Œ์ŠคํŠธ.txt")).exists();
    assertThat(Path.of(dstPath.toString(), "test-dir", "test-dir2", "file2.txt")).exists();
    assertThat(Path.of(dstPath.toString(), "test-dir", "test-dir2", "test-dir3", "ํ•œ๊ธ€์ด๋ฆ„_ํ…Œ์ŠคํŠธ.txt")).exists();
    assertThat(Path.of(dstPath.toString(), "test-dir", "test-dir2", "test-dir3", "file2.txt")).exists();
    assertThat(Path.of(dstPath.toString(), "test-dir", "test-dir2", "test-dir3", "test-dir4", "ํ•œ๊ธ€์ด๋ฆ„_ํ…Œ์ŠคํŠธ.txt")).exists();
    assertThat(Path.of(dstPath.toString(), "test-dir", "test-dir2", "test-dir3", "test-dir4", "file2.txt")).exists();
}

Unzip ํ…Œ์ŠคํŠธ๋„ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. unzipํ•˜๋ ค๋Š” Zip ํŒŒ์ผ๊ณผ, ์••์ถ• ํ•ด์ œํ•  ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋„˜๊ฒจ์„œ ์••์ถ•ํ•ด์ œํ•˜๊ณ  ๋‚ด๋ถ€์— ํŒŒ์ผ๋“ค์ด ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋๋Š”์ง€ ๊ฒ€์ฆ ํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ๋Š” ์„ฑ๊ณต์ ์œผ๋กœ ์ˆ˜ํ–‰๋ฉ๋‹ˆ๋‹ค.

3. Directory ๋‹ค์šด๋กœ๋“œ API ๊ตฌํ˜„


@GetMapping(value = "/api/v1/file-zipping-and-download")
public void downloadZipFile(
        @RequestParam(value = "folderName") String folderName,
        HttpServletResponse response
) {
    try {
        byte[] bytes = ZipUtils.zipFile(Path.of(folderName).toFile());
        response.setContentType("application/zip");
        response.setHeader("Content-Disposition", "attachment; filename=".concat(UUID.randomUUID().toString()).concat(".zip"));
        response.getOutputStream().write(bytes);
    } catch (IOException e) {
        throw new RuntimeException("Failed to download zip file", e);
    }
}

๋‹ค์šด๋กœ๋“œ ํ•˜๋ ค๋Š” ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ๋ฅผ ์ž…๋ ฅ๋ฐ›์•„ ์šฐ๋ฆฌ๊ฐ€ ๊ตฌํ˜„ํ•œ zipFile ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋ฐ˜ํ™˜ ๋ฐ›์€ byte[]๋ฅผ HttpServletResponse์˜ outputStream์„ ํ†ตํ•ด ์จ์ฃผ๋ฉด ํด๋ผ์ด์–ธํŠธ๋Š” ์ž๋™์ ์œผ๋กœ ๋‹ค์šด๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ค‘์š”ํ•œ ์ ์€ ContentType๊ณผ Header๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •ํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ๋Š” ๊ฐ„๋‹จํ•˜๊ฒŒ ํ…Œ์ŠคํŠธ ํ•  ๋ชฉ์ ์œผ๋กœ Controller์— ๋กœ์ง์„ ์ž‘์„ฑํ–ˆ๋Š”๋ฐ ๋”ฐ๋กœ Service ๋ ˆ์ด์–ด๋กœ ์˜ฎ๊ธฐ๋Š”๊ฒŒ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฐ ์œ ์ง€๋ณด์ˆ˜ํ•  ๋•Œ๋„ ํŽธ๋ฆฌํ•  ๊ฒƒ ์ž…๋‹ˆ๋‹ค.

3.1. Directory ๋‹ค์šด๋กœ๋“œ API ํ…Œ์ŠคํŠธ


@WebMvcTest(controllers = ZipUtilController.class)
class ZipUtilControllerTest {
    @Autowired private MockMvc mockMvc;

        @DisplayName("Zip Download Test")
        @Test
        void downloadZipFileTest() throws Exception {
            // given

            // when & then
            mockMvc.perform(get("/api/v1/file-zipping-and-download")
                            .param("folderName", "src/test/resources/test-dir"))
                    .andDo(print())
                    .andExpect(status().isOk())
            ;
        }
}

MockMvc๋ฅผ ์ด์šฉํ•ด์„œ zipFile Download ํ…Œ์ŠคํŠธ๋ฅผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

4. Zip ํŒŒ์ผ ์—…๋กœ๋“œ API ๊ตฌํ˜„


@PostMapping(value = "/api/v1/zip-upload", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> uploadZipFile(
        @RequestParam("dstPath") String dstPath,
        @RequestPart("file") MultipartFile file
) {
    try {
        ZipUtils.unZipFile(dstPath, file.getInputStream());
    } catch (IOException e) {
        return ResponseEntity.ok("failed to upload zip");
    }
    return ResponseEntity.ok("success to upload zip");
}

์—…๋กœ๋“œ API๋Š” Multipart๋ฅผ ์ด์šฉํ•ด์„œ ์—…๋กœ๋“œ๋ฅผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ API ์ฒ˜๋Ÿผ ๋‹จ์ผ MultipartFile๋กœ ๊ตฌ์„ฑํ•ด๋„ ๋˜๊ณ , List๋ฅผ ํ†ตํ•ด ์—ฌ๋Ÿฌ ๊ฐœ๋ฅผ ๋ฐ›์•„์„œ ๋‚ด๋ถ€์—์„œ Zippingํ•˜์—ฌ ์ €์žฅํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ๋Š” Zip ํŒŒ์ผ์„ ์—…๋กœ๋“œ ๋ฐ›๋Š”๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ณ  ZIp ํŒŒ์ผ์„ ๋ฐ›์•„์„œ ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  unZipFile ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž๋กœ๋ถ€ํ„ฐ ์ž…๋ ฅ๋ฐ›์€ ์ €์žฅ ๊ฒฝ๋กœ์— ์••์ถ•ํ•ด์ œ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;

@DisplayName("Zip Upload Test")
@Test
void uploadZipFileTest() throws Exception {
    // given
    MockMultipartFile file = new MockMultipartFile("file", "test.zip", MULTIPART_FORM_DATA_VALUE, (byte[]) null);

    // when & then
    mockMvc.perform(multipart("/api/v1/zip-upload")
                    .file(file)
                    .param("dstPath", "test-dir"))
            .andDo(print())
            .andExpect(status().isOk())
    ;
}

MultipartFile ์—…๋กœ๋“œ API ํ…Œ์ŠคํŠธ๋Š” MockMvcRequestBuilders.multipart ๋ฅผ ํ†ตํ•ด์„œ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. MultipartFile์€ MockMultipartFile์„ ์ด์šฉํ•ด์„œ Mockingํ•˜์—ฌ ํ…Œ์ŠคํŠธํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

5. ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…


5.1. MacOS์—์„œ ์ƒ์„ฑํ•œ zip์€ ๋‚ด๋ถ€์— ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์ˆจ์–ด์žˆ๋‹ค? (__**MAC_OS__**)


MacOS์—์„œ๋Š” Zip ํŒŒ์ผ๋“ค์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋“ค์„ __MAC_OS__ ๋‚ด๋ถ€์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. (stack overflow ์ฐธ์กฐ)

๊ทธ๋ž˜์„œ ํ˜น์‹œ ๋ชจ๋ฅผ Mac ์œ ์ €๋“ค์„ ๋Œ€๋น„ํ•ด ํ•ด๋‹น ํด๋”์— ๋Œ€ํ•ด ์˜ˆ์™ธ์ฒ˜๋ฆฌํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

private static void unZipFile(String dstPath, InputStream inputStream, @Nullable Function<Path, Path> renameDuplicatedFilename) throws IOException {
  try (ZipInputStream zipInputStream = new ZipInputStream(inputStream, Charset.forName("EUC-KR"))) {
      ZipEntry entry;
            ...

      while ((entry = zipInputStream.getNextEntry()) != null) {
          final String entryName = entry.getName();
          if (entry.isDirectory() || entryName.startsWith("__MACOSX")) continue;

                    ...
      }
  }
}
  • entryName.startsWith("__MACOSX")๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ์ด๋ฆ„์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ํด๋”๋Š” ํŒจ์Šคํ•˜๊ฒŒ๋” ์˜ˆ์™ธ ์ฒ˜๋ฆฌํ•ด์คฌ์Šต๋‹ˆ๋‹ค.

What's inside the __MACOSX hidden folder in zip files created in Mac OS

[Java] zip ํŒŒ์ผ ๋‚ด ๊ตฌ์„ฑ ํ™•์ธํ•˜๋Š” ์ฝ”๋“œ

5.2. ์œˆ๋„์šฐ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” ํ™•์žฅ์ž๊ฐ€ appication/zip์ด ์•„๋‹ˆ๋ผ application/x-zip-compressed์ด๋ผ๊ณ ?


์œˆ๋„์šฐ์—์„œ๋Š” ZIP ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๋ฉด ๋ธŒ๋ผ์šฐ์ €์—์„œ application/zip์ด ์•„๋‹Œ application/x-zip-compressed ๊ฐ’์„ ๋„˜๊ฒจ์ค๋‹ˆ๋‹ค.

๋งŒ์•ฝ ์—…๋กœ๋“œ ํ˜•์‹์„ ์ œํ•œํ•œ๋‹ค๋ฉด, ์œˆ๋„์šฐ ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•ด application/x-zip-compressed Content-Type๋„ ํ—ˆ์šฉํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

zip mime types, when to pick which one

5.3. ์œˆ๋„์šฐ์—์„œ ์—…๋กœ๋“œํ•œ ํŒŒ์ผ์„ unZipํ–ˆ์„ ๋•Œ, ๋‚ด๋ถ€ ํŒŒ์ผ๋ช…๋“ค์ด ํŠน์ˆ˜๋ฌธ์ž๋กœ ๋ณด์ผ ๋•Œ (์ธ์ฝ”๋”ฉ ํ˜•์‹ ์•ˆ๋งž์Œ)


์ฐพ์•„๋ณด๋‹ˆ ์œˆ๋„์šฐ์—์„œ๋Š” EUC-KR ๋ญ์‹œ๊ธฐ๋กœ ์ธ์ฝ”๋”ฉ์„ ํ–ˆ..๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค..

ํ•˜..

private static void unZipFile(String dstPath, InputStream inputStream, @Nullable Function<Path, Path> renameDuplicatedFilename) throws IOException {
  try (ZipInputStream zipInputStream = new ZipInputStream(inputStream, Charset.forName("EUC-KR"))) {
        ...
  }
}

์ธ์ฝ”๋”ฉ๋งŒ ์ ์šฉํ•ด์ฃผ๋ฉด ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์–ด๋ ค์šธ ๊ฒƒ์€ ์—†์Šต๋‹ˆ๋‹ค. ZipInputStream์„ ์ƒ์„ฑํ•  ๋•Œ, Charset.forName("EUC-KR") ์ธ์ฝ”๋”ฉ์„ ํ†ตํ•ด ์ƒ์„ฑํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

Java char set encoding problem(from UTF8 to cp866)

Java // unzip error :MALFORMED

Outro


Spring MVC์—์„œ ์ œ๊ณตํ•ด์ฃผ๋Š” MultipartFile ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ด์šฉํ•˜์—ฌ API ๊ตฌํ˜„, ๊ทธ๋ฆฌ๊ณ  ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋ณธ Zip, UnZip Util ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด๊ณ  ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด๋ดค์Šต๋‹ˆ๋‹ค. Zip, UnZip ์œ ํ‹ธ ํด๋ž˜์Šค๋Š” ์ด๋ฏธ ์œ ๋ช…ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(ex. apache.common.io ๋“ฑ)๊ฐ€ ๋งŽ์€๋ฐ์š”, ์ด๋Ÿฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์˜์กดํ•˜๋Š” ๊ฒƒ๋ณด๋‹จ ๊ธฐ๋ณธ JDK API๋ฅผ ํ†ตํ•ด ๊ตฌํ˜„ํ•˜๋Š”๊ฒŒ ๋‚˜์ค‘์— ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ณด์•ˆ ์ทจ์•ฝ์ , ์—…๋ฐ์ดํŠธ์˜ ๋ถ€์žฌ ๋“ฑ๋“ฑ์— ๋Œ€์ฒ˜ํ•˜๊ธฐ ์–ด๋ ต๊ธฐ ๋•Œ๋ฌธ์— ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋Š”๊ฒŒ ์ข‹๋‹ค๊ณ  ์ƒ๊ฐ์ด ๋“ญ๋‹ˆ๋‹ค.

๊ทธ๋ ‡๊ฒŒ ๋Œ€๋‹จํ•œ ๊ธฐ๋Šฅ์€ ์•„๋‹ˆ์ง€๋งŒ์š”..

์•„์ง API ํ…Œ์ŠคํŠธ ํ•˜๋Š” ๋ถ€๋ถ„์ด ์กฐ๊ธˆ ๋ฏธํกํ•˜๊ณ  ์•„์‰ฝ์Šต๋‹ˆ๋‹ค. ์ด ๋ถ€๋ถ„์€ ๋” ์ข‹์€ ๋ฐฉ๋ฒ•์ด ์žˆ์œผ๋ฉด ์ถ”ํ›„์— ์—…๋ฐ์ดํŠธ ํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

๊ธด ๊ธ€ ์ฝ์–ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!

 

๋ฐ˜์‘ํ˜•