CodeGym /행동 /JAVA 25 SELF /아카이브/압축: java.util.zip

아카이브/압축: java.util.zip

JAVA 25 SELF
레벨 41 , 레슨 4
사용 가능

1. 소개: 왜 Java에서 아카이브와 압축이 필요한가

현대에는 아카이브와 압축 파일을 다루는 일이 흔한 작업입니다: 백업, 파일 교환, 로깅, 대용량 데이터 저장 등. Java는 java.util.zip 패키지를 통해 ZIP 형식 아카이브와 GZIP 압축 파일을 다루는 표준 수단을 제공합니다.

Java로 할 수 있는 것:

  • ZIP 아카이브(다중 파일 컨테이너)를 읽고 생성하기.
  • GZIP 파일(단일 파일 압축)을 읽고 생성하기.
  • 아카이브 내용을 관리하고, 마스크로 파일을 필터링하기.
  • 압축 수준 제어.
  • 압축 해제 시 보안 점검(zip slipzip bomb 방지).

2. 핵심 클래스

ZipInputStream 및 ZipOutputStream

ZIP 아카이브를 순차적으로 읽고/작성하는 스트림 클래스입니다. 사용 시점: 개별 파일에 대한 랜덤 액세스 없이 아카이브를 “스트리밍”으로 읽거나 생성해야 할 때.

예시: 아카이브 읽기

import java.io.*;
import java.util.zip.*;

try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {
    ZipEntry entry;
    while ((entry = zis.getNextEntry()) != null) {
        System.out.println("파일: " + entry.getName());
        // zis.read(...)를 통해 entry의 내용을 읽을 수 있습니다
        zis.closeEntry();
    }
}

예시: 아카이브 생성

import java.io.FileOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip"))) {
    ZipEntry entry = new ZipEntry("hello.txt");
    zos.putNextEntry(entry);
    zos.write("안녕, 아카이브!".getBytes());
    zos.closeEntry();
}

ZipFile

ZIP 아카이브 내용을 랜덤 액세스로 다루는 클래스입니다: 파일 목록을 빠르게 얻고, 이름으로 임의의 파일을 열어 내용을 읽을 수 있습니다.

예시:

import java.util.zip.*;
import java.io.*;

ZipFile zipFile = new ZipFile("archive.zip");
zipFile.stream().forEach(entry -> System.out.println(entry.getName()));

ZipEntry entry = zipFile.getEntry("hello.txt");
try (InputStream is = zipFile.getInputStream(entry)) {
    // 파일 내용을 읽습니다
}
zipFile.close();

ZipFile을 언제 사용할까?

  • 파일 목록, 메타데이터, 크기, 날짜를 빠르게 얻어야 할 때.
  • 아카이브 전체를 순차적으로 훑지 않고 개별 파일을 읽어야 할 때.

ZipEntry

아카이브 내부의 개별 파일이나 폴더를 나타내는 객체. 이름, 크기, 날짜, 플래그, 압축 수준 등을 포함합니다.

import java.util.zip.ZipEntry;

ZipEntry entry = new ZipEntry("docs/readme.txt");
entry.setComment("파일 설명");
entry.setTime(System.currentTimeMillis());

압축 수준(Deflater)

아카이브를 생성할 때 압축 정도를 제어할 수 있습니다( 0 — 무압축, 9 — 최대 압축):

import java.io.FileOutputStream;
import java.util.zip.Deflater;
import java.util.zip.ZipOutputStream;

try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip"))) {
    zos.setLevel(Deflater.BEST_COMPRESSION); // 또는 0..9
    // ...
}
  • Deflater.NO_COMPRESSION (0)
  • Deflater.BEST_SPEED (1)
  • Deflater.BEST_COMPRESSION (9)
  • Deflater.DEFAULT_COMPRESSION (-1)

원칙: 수준이 높을수록 더 느리지만 압축률이 높습니다.

3. 단일 파일 압축

GZIP은 하나의 파일(아카이브가 아님!)을 압축하기 위한 포맷입니다. 로그, 임시 파일, 네트워크 전송 등에 사용됩니다.

예시: 파일 압축

import java.util.zip.*;
import java.io.*;

try (GZIPOutputStream gos = new GZIPOutputStream(new FileOutputStream("file.txt.gz"));
     FileInputStream fis = new FileInputStream("file.txt")) {
    fis.transferTo(gos);
}

예시: 파일 압축 해제

import java.util.zip.GZIPInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

try (GZIPInputStream gis = new GZIPInputStream(new FileInputStream("file.txt.gz"));
     FileOutputStream fos = new FileOutputStream("file.txt")) {
    gis.transferTo(fos);
}

기억하세요: GZIP은 하나의 파일만 처리합니다 — 폴더 구조나 추가 메타데이터를 보존하지 않습니다. 반면 ZIP은 여러 파일과 전체 폴더를 구조와 각 항목의 정보를 유지하면서 함께 압축할 수 있습니다.

4. 디렉터리 압축/해제, PathMatcher 기반 필터

디렉터리를 ZIP으로 압축하기

파일과 하위 폴더를 포함한 폴더를 압축하려면, 파일 트리를 재귀적으로 순회하고 각 파일을 올바른 상대 경로로 아카이브에 추가합니다(ZIP의 구분자는 항상 "/").

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

Path sourceDir = Paths.get("myfolder");
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip"))) {
    Files.walk(sourceDir)
        .filter(Files::isRegularFile)
        .forEach(path -> {
            String entryName = sourceDir.relativize(path).toString().replace("\\", "/");
            try (InputStream is = Files.newInputStream(path)) {
                zos.putNextEntry(new ZipEntry(entryName));
                is.transferTo(zos);
                zos.closeEntry();
            } catch (IOException e) { e.printStackTrace(); }
        });
}

아카이브를 디렉터리로 압축 해제

import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {
    ZipEntry entry;
    while ((entry = zis.getNextEntry()) != null) {
        Path outPath = Paths.get("output", entry.getName());
        if (entry.isDirectory()) {
            Files.createDirectories(outPath);
        } else {
            Files.createDirectories(outPath.getParent());
            try (OutputStream os = Files.newOutputStream(outPath)) {
                zis.transferTo(os);
            }
        }
        zis.closeEntry();
    }
}

마스크로 파일 필터링(PathMatcher)

예를 들어 "*.txt"만 대상으로 압축/해제를 수행하도록 파일을 마스크로 필터링할 수 있습니다:

import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.PathMatcher;

PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**/*.txt");
Files.walk(sourceDir)
    .filter(matcher::matches)
    .forEach(/* ... */);

5. 보안: Zip Slip, zip bomb, 경로 정규화 검사

Zip Slip(경로 조작 공격)

문제: 공격자가 파일 이름을 "../../../../etc/passwd"와 같이 만든 아카이브를 생성할 수 있습니다. 검증 없이 압축을 풀면 시스템 파일이 덮어쓰일 수 있습니다!

해결: 파일을 쓰기 전에 경로를 정규화하고 대상 디렉터리를 벗어나지 않는지 확인하세요.

import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

Path targetDir = Paths.get("output").toAbsolutePath();
try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {
    ZipEntry entry;
    while ((entry = zis.getNextEntry()) != null) {
        Path outPath = targetDir.resolve(entry.getName()).normalize();
        if (!outPath.startsWith(targetDir)) {
            throw new IOException("Zip Slip: 대상 폴더 밖에 쓰기를 시도했습니다!");
        }
        if (entry.isDirectory()) {
            Files.createDirectories(outPath);
        } else {
            Files.createDirectories(outPath.getParent());
            try (OutputStream os = Files.newOutputStream(outPath)) {
                zis.transferTo(os);
            }
        }
        zis.closeEntry();
    }
}

Zip bomb(압축 폭탄)

문제: 아카이브는 몇 킬로바이트에 불과하지만, 압축을 풀면 파일 하나가 기가바이트를 차지할 수 있습니다. 이는 서버나 디스크를 마비시킬 수 있습니다.

해결: 압축 해제할 개별 파일의 최대 크기와 전체 해제 용량을 제한하고, 한도를 초과하면 과정을 중단하세요.

import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

long maxSize = 100 * 1024 * 1024; // 100 MB
long totalUnzipped = 0;

try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {
    ZipEntry entry;
    while ((entry = zis.getNextEntry()) != null) {
        Path outPath = Paths.get("output", entry.getName()).normalize();
        Files.createDirectories(outPath.getParent());

        long written = 0;
        try (OutputStream os = Files.newOutputStream(outPath)) {
            byte[] buf = new byte[8192];
            int len;
            while ((len = zis.read(buf)) > 0) {
                os.write(buf, 0, len);
                written += len;
                totalUnzipped += len;
                if (written > maxSize || totalUnzipped > maxSize) {
                    throw new IOException("Zip bomb detected!");
                }
            }
        }
        zis.closeEntry();
    }
}

6. 실습: 마스크를 지원하는 CLI 유틸리티 “zip/unzip”

마스크를 지원하는 파일 압축/해제용 간단한 콘솔 유틸리티를 작성해 봅니다.

예시: 압축

// java ZipUtil zip myfolder archive.zip *.txt
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public static void zip(String sourceDir, String zipFile, String glob) throws IOException {
    Path src = Paths.get(sourceDir);
    PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + glob);
    try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
        Files.walk(src)
            .filter(Files::isRegularFile)
            .filter(matcher::matches)
            .forEach(path -> {
                String entryName = src.relativize(path).toString().replace("\\", "/");
                try (InputStream is = Files.newInputStream(path)) {
                    zos.putNextEntry(new ZipEntry(entryName));
                    is.transferTo(zos);
                    zos.closeEntry();
                } catch (IOException e) { e.printStackTrace(); }
            });
    }
}

예시: Zip Slip 방어 포함 압축 해제

// java ZipUtil unzip archive.zip output
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public static void unzip(String zipFile, String outDir) throws IOException {
    Path targetDir = Paths.get(outDir).toAbsolutePath();
    try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
        ZipEntry entry;
        while ((entry = zis.getNextEntry()) != null) {
            Path outPath = targetDir.resolve(entry.getName()).normalize();
            if (!outPath.startsWith(targetDir)) {
                throw new IOException("Zip Slip: 대상 폴더 밖에 쓰기를 시도했습니다!");
            }
            if (entry.isDirectory()) {
                Files.createDirectories(outPath);
            } else {
                Files.createDirectories(outPath.getParent());
                try (OutputStream os = Files.newOutputStream(outPath)) {
                    zis.transferTo(os);
                }
            }
            zis.closeEntry();
        }
    }
}

실행 예:

java ZipUtil zip myfolder archive.zip "*.txt"
java ZipUtil unzip archive.zip output

첫 번째 예제에서 명령 java ZipUtil zip myfolder archive.zip "*.txt"는 폴더 myfolder의 모든 .txt 파일을 archive.zip 아카이브로 압축합니다. 두 번째 예제 java ZipUtil unzip archive.zip output은 아카이브를 output 폴더로 풀며, 어떤 파일도 대상 디렉터리 밖에 기록되지 않도록 검사합니다 — 이것이 바로 Zip Slip 방어입니다.

1
설문조사/퀴즈
IO 최적화, 레벨 41, 레슨 4
사용 불가능
IO 최적화
IO 최적화
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION