Async Zip in .NET 10: 한 줄로 생성 또는 추출
C#에서 zip 파일 작업은 항상 System.IO.Compression를 통해 가능했지만, 모든 호출은 동기적이어서 아카이브가 완전히 쓰이거나 읽힐 때까지 스레드가 차단되었습니다. .NET 10은 이를 비동기 오버로드를 설정하여, 지퍼 파일을 생성, 추출 및 채울 수 있게 함으로써 호출 스레드를 묶어두지 않는 방식으로 변화시킵니다.
이번 안내에서는 Tim Corey의 최신 가이드를 기반으로 .NET 10에서의 새로운 비동기 zip 오버로드를 시연합니다. 우리는 아카이브를 빌드하는 한 줄의 코드, 그것을 풀기 위한 단일 호출, 그리고 완전한 제어를 위한 수동 방법의 세 가지 점점 더 자세한 접근 방식을 살펴볼 것입니다. 또한 using 범위의 함정을 조사할 것입니다.
셋업: 경로 및 소스 폴더
[0:28 - 1:55] 설정은 .NET 10을 대상으로 하는 콘솔 애플리케이션과 단일 using 지시어로 시작됩니다:
using System.IO.Compression;using System.IO.Compression;세 개의 문자열 변수는 데모 전체에서 사용되는 경로를 정의합니다:
string sourceDirectory = @"C:\temp\test";
string destinationZipFile = @"C:\temp\archive.zip";
string destinationDirectory = @"C:\temp\extracted";string sourceDirectory = @"C:\temp\test";
string destinationZipFile = @"C:\temp\archive.zip";
string destinationDirectory = @"C:\temp\extracted";문자열 접두사 '@'는 백슬래시를 두 번 처리해야 할 필요를 피할 수 있습니다. sourceDirectory는 zip으로 묶을 폴더입니다. destinationZipFile는 생성될 아카이브의 전체 경로입니다. destinationDirectory는 추출될 때 내용이 저장될 위치입니다.
실용적인 경고: 절대 destinationZipFile을 sourceDirectory 내부 경로로 지정하지 마세요. 압축 중인 폴더에 ZIP을 쓰는 것은 프로세스를 중단시키는 재귀적 읽기 루프를 유발합니다.
테스트 폴더에는 루트에 두 개의 파일과 하위 폴더 내부에 세 번째 파일이 있으며, 이는 수동 접근 방식에서 includeBaseDirectory 옵션과 상대 경로 처리를 탐색할 때 중요합니다.
1줄로 아카이브 생성
[2:35 - 4:20] 비동기 생성 호출은 동기 ZipFile.CreateFromDirectory의 직접 대체물입니다:
await ZipFile.CreateFromDirectoryAsync(
sourceDirectory,
destinationZipFile,
CompressionLevel.SmallestSize,
includeBaseDirectory: false);await ZipFile.CreateFromDirectoryAsync(
sourceDirectory,
destinationZipFile,
CompressionLevel.SmallestSize,
includeBaseDirectory: false);CompressionLevel.SmallestSize은 처리 시간을 조금 더 사용하여 가장 작은 출력에 우선순위를 둡니다. CompressionLevel.Fastest은 그 반대를 수행합니다. 대부분의 개발 시나리오에서는 차이가 미미하지만, 많은 아카이브를 동시에 처리하는 웹 서버에서는 이러한 절충안을 평가해볼 가치가 있습니다.
includeBaseDirectory은 원본 디렉토리 이름이 아카이브 내부의 루트 항목으로 나타나는지를 제어합니다. 기본값인 false으로 설정하면, zip은 직접 파일로 열립니다. true를 전달하면 루트에 test 폴더가 생성되고 실제 파일은 그 내부에 위치하게 됩니다. 대부분의 사용 사례에서는 이를 false으로 설정하면 이점이 있습니다.
한 줄로 아카이브 추출하기
[4:45 - 5:55] 추출은 동일한 패턴을 따릅니다:
await ZipFile.ExtractToDirectoryAsync(
destinationZipFile,
destinationDirectory,
overwriteFiles: false);await ZipFile.ExtractToDirectoryAsync(
destinationZipFile,
destinationDirectory,
overwriteFiles: false);destinationDirectory는 이미 존재하지 않는 경우 자동으로 생성됩니다. overwriteFiles 매개 변수는 기본적으로 false로 설정되며, 아카이브의 파일이 이미 대상 경로에 존재할 경우 예외를 던집니다. 기존 파일을 조용히 대체하려면 true로 설정하세요. 추출을 두 번 실행하면 이를 보여줍니다: 첫 번째 실행은 성공적으로 폴더를 생성하고, 두 번째 실행은 overwriteFiles: true이 지정되지 않으면 예외를 던집니다.
선택적 압축: 파일 하나씩 추가하기
[6:15 - 9:50] 한 줄 방식은 필터링 없이 전체 디렉토리를 압축합니다. 특정 파일만 포함해야 할 때, FileStream과 ZipArchive을 사용하여 아카이브를 수동으로 작성합니다:
await using FileStream zipStream = new FileStream(
destinationZipFile,
FileMode.Create,
FileAccess.Write,
FileShare.None,
bufferSize: 4096,
useAsync: true);
using ZipArchive archive = await ZipArchive.CreateAsync(
zipStream,
ZipArchiveMode.Create,
leaveOpen: false,
entryNameEncoding: null);await using FileStream zipStream = new FileStream(
destinationZipFile,
FileMode.Create,
FileAccess.Write,
FileShare.None,
bufferSize: 4096,
useAsync: true);
using ZipArchive archive = await ZipArchive.CreateAsync(
zipStream,
ZipArchiveMode.Create,
leaveOpen: false,
entryNameEncoding: null);여기서는 이해할 가치가 있는 몇 가지 매개변수가 있습니다. FileMode.Create는 해당 경로에 존재하는 모든 파일을 덮어씁니다. 파일이 이미 존재하는 경우 FileMode.CreateNew가 대신 예외를 던집니다. FileShare.None는 아카이브가 작성되는 동안 파일을 배타적으로 잠글 수 있으며, 다른 프로세스가 중간에 파일을 읽거나 쓰지 못하게 방지합니다.
FileStream의 useAsync: true는 OS 수준에서 비동기 I/O를 가능하게 합니다. 이것을 설정하되 실제로 하위에서 비동기 호출을 사용하지 않으면 프로세스가 크게 느려질 수 있으며, 때로는 10배까지 느려질 수 있습니다. 항목 작성 루프가 await를 사용하기 때문에 useAsync: true이 여기에 적합한 선택입니다. 동기 코드 경로에서는 기본 false로 그대로 두세요.
ZipArchive의 leaveOpen: false는 해제될 때 기본 스트림을 닫고 플러시하도록 지시합니다. entryNameEncoding: null는 기본 인코딩을 유지하며, 문서에서는 특별한 이유가 없는 한 이를 변경하지 않도록 권장합니다.
아카이브가 준비되면, 원본 내용을 검색하여 각 항목을 씁니다:
string[] files = Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories);
foreach (string filePath in files)
{
string relativePathAndName = Path.GetRelativePath(sourceDirectory, filePath);
await archive.CreateEntryFromFileAsync(filePath, relativePathAndName);
}string[] files = Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories);
foreach (string filePath in files)
{
string relativePathAndName = Path.GetRelativePath(sourceDirectory, filePath);
await archive.CreateEntryFromFileAsync(filePath, relativePathAndName);
}Directory.GetFiles와 함께 SearchOption.AllDirectories는 중첩된 하위 폴더에서 파일을 검색합니다. 이로 인해 아카이브에 하위 폴더 구조가 보존되는 것입니다. 패턴 인자 ("*")는 확장자로 필터링할 수 있습니다: 예를 들어 "*.txt"는 아카이브를 텍스트 파일로 제한합니다.
Path.GetRelativePath는 각 파일 경로에서 절대 접두사를 제거하여 sourceDirectory에 상대적인 부분만 남깁니다. 이는 아카이브 내 항목 이름으로 저장되며, 하위 폴더 계층 구조를 정확히 복제합니다. Path.GetFileName(filePath)를 대신 전달하면 해당 항목이 원래 위치와 상관없이 zip의 루트에 놓입니다. 이는 평면 아카이브를 생성하지만 서로 다른 하위 폴더에서 파일 이름이 동일한 두 항목이 있을 경우 이름 충돌 위험이 있습니다.
범위 사용 시의 함정
[9:50 - 11:30] 수동 zip 블록 뒤에 추출 호출을 추가하려고 하면 파일 잠금 예외가 발생합니다: The process cannot access the file because it is being used by another process. 이는 zipStream에 대한 using 문이 파일 범위 구문을 사용하기 때문입니다. 즉, 스트림이 zip 블록의 중괄호에서 닫히지 않고 파일 끝까지 열려 있게 됩니다. 추출 시도는 잠금이 여전히 유지되는 동안 실행됩니다.
중괄호 형태로 변환하면 다음과 같이 해결됩니다:
// Before (file-scoped: stream stays open until end of file)
await using FileStream zipStream = new FileStream(...);
// After (block-scoped; stream released at the closing brace)
await using (FileStream zipStream = new FileStream(...))
{
// zip operations here
}
// stream is now closed; extraction can proceed safely// Before (file-scoped: stream stays open until end of file)
await using FileStream zipStream = new FileStream(...);
// After (block-scoped; stream released at the closing brace)
await using (FileStream zipStream = new FileStream(...))
{
// zip operations here
}
// stream is now closed; extraction can proceed safelyZIP 작업을 중괄호로 감싸고 마지막 세미콜론을 제거하면 스트림의 수명을 명시적 범위에 한정합니다. 종료 중괄호를 벗어나면 잠금이 해제되어 후속 추출 호출이 충돌 없이 동일한 파일을 열 수 있게 됩니다.
결론
[11:40 - 끝] 한 줄의 코드는 일반적인 경우를 처리합니다: 전체 폴더를 아카이브하기 위한 CreateFromDirectoryAsync와 이를 풀기 위한 ExtractToDirectoryAsync. 아카이브에 들어갈 파일을 제어해야 할 때, 수동 FileStream 및 ZipArchive 접근 방식은 항목 수준에서 필터링, 이름 변경, 경로 재작성 가능성을 제공합니다.
요약: using System.IO.Compression를 추가하고, 간단한 경우에는 await ZipFile.CreateFromDirectoryAsync 또는 await ZipFile.ExtractToDirectoryAsync를 호출하며, 항목 필터링이나 이름 변경이 필요할 때는 수동 ZipArchive 경로를 사용하세요. 스트림을 후속 코드 실행 전에 해제해야 한다면 using 블록을 중괄호로 범위 설정하세요. 이러한 추가 사항은 .NET 10에서 전체 ZIP 워크플로우 전반에 걸쳐 async/await 패턴을 사용할 수 있도록 합니다.
Tim Corey의 유튜브 동영상에서 생중계 코딩과 함께 끝까지 따라갈 수 있습니다.

