
만약 1GB짜리 거대한 로그 파일을 읽는 동안 프로그램이 멈춰버린다면 어떻게 될까요?
상상만 해도 끔찍하죠? 이번 글에서는 이 문제를 해결하는 가장 올바른 방법,
ReadAsync와 WriteAsync에 대해 알아보겠습니다.
먼저 기존의 동기 방식 파일 읽기가 왜 문제가 되는지부터 살펴보겠습니다.
비유: 자료실의 사서
동기 파일 I/O는 자료실에 가서 사서에게 희귀 도서를 요청하는 것과 같습니다.
사서는 지하 서고로 내려가고, 여러분은 사서가 책을 찾아 돌아올 때까지 앞에서
꼼짝도 못 하고 기다려야 합니다. 다른 책을 구경하거나 자리에 가서 쉴 수도 없죠.
데이터(희귀 도서)를 찾아줄 때까지 아무 일도 못 하고 자원만 낭비하게 됩니다.
[코드]
using System;
using System.IO;
using System.Diagnostics;
class Program
{
static void Main()
{
// --- 테스트 환경 구성 ---
string sourceDir = @"C:\Temp";
string filePath = @"C:\Temp\large_file.log";
Directory.CreateDirectory(sourceDir); // 예제를 위해 폴더 생성
string data = new string('A', 100_000_000); // 100MB 크기의 로그
File.WriteAllText(Path.Combine(sourceDir, "large_file.log"), data);
// --- 테스트 환경 구성 끝 ---
Console.WriteLine("동기 파일 읽기 시작...");
var stopwatch = Stopwatch.StartNew();
// 이 라인에서 스레드는 파일 읽기가 끝날 때까지 '블로킹'된다.
// UI 앱이라면 화면이 그대로 멈춘다!
string fileContent = File.ReadAllText(filePath);
stopwatch.Stop();
Console.WriteLine($"파일 읽기 완료. {stopwatch.ElapsedMilliseconds}ms 소요");
Console.WriteLine($"파일 크기: {fileContent.Length:N0} bytes");
}
}
[실행 결과]
동기 파일 읽기 시작...
파일 읽기 완료. 426ms 소요
파일 크기: 100,000,000 bytes
ReadAsync와 WriteAsync는 이 비효율적인 기다림을 해결해 줍니다.
비유: '진동벨'을 주는 똑똑한 사서
비동기 파일 읽기는 똑똑한 사서에게 희귀 도서를 요청하는 것과 같습니다.
이 똑똑한 사서는 요청을 받자마자 여러분에게 '진동벨(Task)'을 줍니다.
"책을 찾으면 벨을 울려드릴 테니, 그동안 다른 책을 보시거나 편히 쉬고 계세요."
이제 여러분은 자유롭게 다른 일을 할 수 있습니다.
async/await와 함께 사용하는 ReadAsync는 스레드가
디스크 작업을 기다리지 않고 다른 중요한 일을 처리하도록 해줍니다.
ReadAsync를 사용하려면 FileStream이나 StreamReader같은 스트림 객체가 필요합니다.
[코드]
using System;
using System.IO;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
// --- 테스트 환경 구성 ---
string sourceDir = @"C:\Temp";
string filePath = @"C:\Temp\large_file.log";
Directory.CreateDirectory(sourceDir); // 예제를 위해 폴더 생성
string data = new string('A', 100_000_000); // 100MB 크기의 로그
File.WriteAllText(Path.Combine(sourceDir, "large_file.log"), data);
// --- 테스트 환경 구성 끝 ---
Console.WriteLine("비동기 파일 읽기 시작...");
var stopwatch = Stopwatch.StartNew();
using (var reader = new StreamReader(filePath, Encoding.UTF8))
{
// await를 만나면, 제어권을 호출자에게 넘겨주고 파일 읽기는 백그라운드에서 진행된다.
// 스레드는 블로킹되지 않고 다른 일을 할 수 있다!
string fileContent = await reader.ReadToEndAsync();
stopwatch.Stop();
Console.WriteLine($"파일 읽기 완료. {stopwatch.ElapsedMilliseconds}ms 소요");
Console.WriteLine($"파일 크기: {fileContent.Length:N0} bytes");
}
}
}
[실행 결과]
비동기 파일 읽기 시작...
파일 읽기 완료. 537ms 소요
파일 크기: 100,000,000 bytes
두 코드의 실행 시간은 비슷하게 나올 수 있지만, 결정적인 차이는
작업을 기다리는 동안 스레드가 다른 일도 할 수 있습니다.
대용량 데이터를 디스크에 쓰는 작업 역시 시간이 걸릴 수 있습니다.
이때 디스크 작업을 기다리지 않고 다른 중요한 일을 처리하도록 해줍니다.
[코드]
using System;
using System.IO;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
// --- 테스트 환경 구성 ---
string sourceDir = @"C:\Temp";
string filePath = @"C:\Temp\new_large.log";
Directory.CreateDirectory(sourceDir); // 예제를 위해 폴더 생성
string data = new string('A', 100_000_000); // 100MB 크기의 로그
// --- 테스트 환경 구성 끝 ---
Console.WriteLine("비동기 파일 쓰기 시작...");
var stopwatch = Stopwatch.StartNew();
// StreamWriter는 IAsyncDisposable을 구현하는 클래스이므로,
// C# 8.0부터는 using문에 await를 붙여서 사용할 수 있습니다.
await using (var writer = new StreamWriter(filePath, append: true))
{
// 디스크에 쓰는 동안 스레드는 자유롭다.
await writer.WriteAsync(data);
stopwatch.Stop();
Console.WriteLine($"파일 쓰기 완료. {stopwatch.ElapsedMilliseconds}ms 소요");
}
}
}
[실행 결과]
비동기 파일 쓰기 시작...
파일 쓰기 완료. 560ms 소요
비동기 파일 I/O는 단순히 UI 멈춤을 방지하는 것 이상의 의미를 가집니다.
특히 ASP.NET Core 같은 웹 프레임워크나 UI에서는 그 중요성이 극대화됩니다.
| 구분 | 동기 I/O (Sync) | 비동기 I/O (Async) |
|---|---|---|
| 스레드 동작 | I/O 작업 동안 블로킹(Blocking) | I/O 작업 동안 다른 요청도 처리 가능 |
| 처리 방식 | 작업을 순서대로 처리 | 작업을 수행하는 동안 다른 작업도 가능 |
| 확장성 | 낮음 (사용자가 늘면 스레드 부족) | 높음 (ASP.NET Core/UI 성능의 핵심) |