Async Zip in .NET 10: 한 줄로 생성 또는 추출
[[academy-video-youtube({"vid": "YgQ3ta6455A", "start_time": "0", "title": "Async Zip in .NET 10: One Line Create or Extract", "creator": "Tim Corey", "length": "~12m"})]]
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은 압축할 폴더입니다. 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는 아카이브가 작성되는 동안 파일을 독점적으로 잠그어 다른 프로세스가 중간 작업 중에 읽거나 쓰지 못하도록 합니다.
useAsync: true은 FileStream에서 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 - end] 일줄 명령은 일반적인 경우를 처리합니다: 전체 폴더를 아카이브하기 위한 CreateFromDirectoryAsync 및 그것을 풀어내기 위한 ExtractToDirectoryAsync. 어떤 파일을 아카이브에 넣을지 제어하고 싶을 때는 수동 FileStream 및 ZipArchive 접근 방식이 항목 수준에서 필터링, 이름 바꾸기 및 경로 재작성 기능을 제공합니다.
요약: using System.IO.Compression을 추가하고, 간단한 경우 await ZipFile.CreateFromDirectoryAsync이나(나) await ZipFile.ExtractToDirectoryAsync를 호출하며, 필터링이나 항목 이름 바꾸기가 필요할 때 수동 ZipArchive 경로를 활용하세요. 스트림이 후속 코드가 실행되기 전에 해제되어야 할 경우, 중괄호로 using 블록의 범위를 정하세요. 이러한 추가 사항은 .NET 10에서 전체 ZIP 워크플로우 전반에 걸쳐 async/await 패턴을 사용할 수 있도록 합니다.
Tim Corey의 유튜브 동영상에서 생중계 코딩과 함께 끝까지 따라갈 수 있습니다.

