[로봇활용_14주차] C# ReadAsync & WriteAsync (비동기 파일 I/O)

최윤호·2025년 11월 9일
post-thumbnail

멈춤 없는 파일 처리

만약 1GB짜리 거대한 로그 파일을 읽는 동안 프로그램이 멈춰버린다면 어떻게 될까요?
상상만 해도 끔찍하죠? 이번 글에서는 이 문제를 해결하는 가장 올바른 방법,
ReadAsyncWriteAsync에 대해 알아보겠습니다.

1)전통적인 파일 I/O

먼저 기존의 동기 방식 파일 읽기가 왜 문제가 되는지부터 살펴보겠습니다.

비유: 자료실의 사서
동기 파일 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

2)비동기 파일 I/O

ReadAsyncWriteAsync는 이 비효율적인 기다림을 해결해 줍니다.

비유: '진동벨'을 주는 똑똑한 사서
비동기 파일 읽기는 똑똑한 사서에게 희귀 도서를 요청하는 것과 같습니다.
이 똑똑한 사서는 요청을 받자마자 여러분에게 '진동벨(Task)'을 줍니다.
"책을 찾으면 벨을 울려드릴 테니, 그동안 다른 책을 보시거나 편히 쉬고 계세요."
이제 여러분은 자유롭게 다른 일을 할 수 있습니다.

async/await와 함께 사용하는 ReadAsync는 스레드가
디스크 작업을 기다리지 않고 다른 중요한 일을 처리하도록 해줍니다.

1. 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

두 코드의 실행 시간은 비슷하게 나올 수 있지만, 결정적인 차이는
작업을 기다리는 동안 스레드가 다른 일도 할 수 있습니다.

2. WriteAsync로 파일 쓰기

대용량 데이터를 디스크에 쓰는 작업 역시 시간이 걸릴 수 있습니다.
이때 디스크 작업을 기다리지 않고 다른 중요한 일을 처리하도록 해줍니다.

[코드]

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 소요

3)정리

비동기 파일 I/O는 단순히 UI 멈춤을 방지하는 것 이상의 의미를 가집니다.
특히 ASP.NET Core 같은 웹 프레임워크나 UI에서는 그 중요성이 극대화됩니다.

구분동기 I/O (Sync)비동기 I/O (Async)
스레드 동작I/O 작업 동안 블로킹(Blocking)I/O 작업 동안 다른 요청도 처리 가능
처리 방식작업을 순서대로 처리작업을 수행하는 동안 다른 작업도 가능
확장성낮음 (사용자가 늘면 스레드 부족)높음 (ASP.NET Core/UI 성능의 핵심)
profile
🚀 미래의 엔지니어를 꿈꾸는 훈련생의 기록 📝

0개의 댓글