IT개발

C#에서의 비동기 프로그래밍과 성능 최적화

HyochulLab 2025. 3. 12. 12:38

C#에서의 비동기 프로그래밍과 성능 최적화

1. 비동기 프로그래밍이란?

비동기 프로그래밍(Asynchronous Programming)은 작업을 병렬적으로 실행하여 응답성을 높이고 성능을 향상시키는 기법입니다. C#에서는 async와 await 키워드를 사용하여 비동기 작업을 구현할 수 있습니다.

비동기 프로그래밍은 특히 다음과 같은 상황에서 유용합니다:

  • I/O 바운드 작업 (파일 읽기/쓰기, 데이터베이스 접근, 네트워크 요청 등)
  • UI 스레드를 차단하지 않고 응답성을 유지해야 하는 경우
  • 대규모 데이터 처리 및 병렬 실행이 필요한 경우

2. C#에서 비동기 프로그래밍 구현하기

2.1 기본적인 async 및 await 사용법

C#에서 비동기 메서드는 async 키워드를 사용하여 선언하고, Task 또는 Task<T>를 반환하도록 만듭니다.

public async Task GetDataAsync()
{
    HttpClient client = new HttpClient();
    string result = await client.GetStringAsync("https://example.com");
    return result;
}

위 코드에서 GetStringAsync는 네트워크 요청을 비동기적으로 수행하며, await 키워드를 통해 결과를 기다립니다.

2.2 Task.Run을 이용한 백그라운드 작업 실행

CPU 바운드 작업의 경우 Task.Run을 사용하여 별도의 스레드에서 실행할 수 있습니다.

public async Task<int> ComputeAsync()
{
    return await Task.Run(() =>
    {
        int sum = 0;
        for (int i = 0; i < 1000000; i++)
            sum += i;
        return sum;
    });
}

이 방식은 UI 스레드를 차단하지 않고 무거운 연산을 수행하는 데 유용합니다.

3. 비동기 프로그래밍 성능 최적화 방법

3.1 ConfigureAwait(false) 사용하기

비동기 작업이 UI 스레드와 관련이 없다면 ConfigureAwait(false)를 사용하여 컨텍스트 전환 비용을 줄일 수 있습니다.

public async Task GetDataOptimizedAsync()
{
    HttpClient client = new HttpClient();
    string result = await client.GetStringAsync("https://example.com").ConfigureAwait(false);
    return result;
}

이렇게 하면 UI 컨텍스트가 아닌 쓰레드 풀(Thread Pool)에서 작업이 완료된 후 바로 반환됩니다.

3.2 불필요한 async/await 피하기

불필요하게 async/await를 사용하면 오버헤드가 발생할 수 있습니다. 단순히 Task를 반환하는 경우에는 직접 반환하는 것이 좋습니다.

// 비효율적인 코드
public async Task<int> GetNumberAsync()
{
    return await Task.FromResult(42);
}

// 최적화된 코드
public Task<int> GetNumberAsync()
{
    return Task.FromResult(42);
}

후자의 코드가 더 효율적이며 불필요한 상태 머신 생성을 방지할 수 있습니다.

3.3 병렬 실행을 활용하기

여러 개의 독립적인 비동기 작업을 실행할 때는 await를 개별적으로 호출하는 대신, Task.WhenAll을 활용하면 더 높은 성능을 얻을 수 있습니다.

public async Task FetchMultipleUrlsAsync()
{
    HttpClient client = new HttpClient();
    var tasks = new[]
    {
        client.GetStringAsync("https://example.com/1"),
        client.GetStringAsync("https://example.com/2"),
        client.GetStringAsync("https://example.com/3")
    };
    
    string[] results = await Task.WhenAll(tasks);
    foreach (var result in results)
    {
        Console.WriteLine(result);
    }
}

이렇게 하면 3개의 HTTP 요청이 동시에 실행되어 성능이 향상됩니다.

3.4 ValueTask 활용하기

작은 크기의 반환값을 갖는 경우 ValueTask<T>를 사용하면 Task<T>보다 오버헤드를 줄일 수 있습니다.

public ValueTask<int> GetSmallNumberAsync()
{
    return new ValueTask<int>(42);
}

ValueTask는 불필요한 힙 할당을 방지하여 성능 최적화에 도움을 줄 수 있습니다.

4. 실전 적용 사례

4.1 대량 데이터 처리 시 Parallel.ForEachAsync 활용

.NET 6 이상에서는 Parallel.ForEachAsync를 활용하여 대량 데이터를 병렬로 처리할 수 있습니다.

public async Task ProcessLargeDataAsync(List<string> dataList)
{
    await Parallel.ForEachAsync(dataList, async (data, cancellationToken) =>
    {
        await ProcessDataAsync(data);
    });
}

이 방법은 ThreadPool을 활용하여 여러 개의 데이터 항목을 동시에 처리하는 데 유용합니다.

4.2 데이터베이스 접근 최적화

EF Core에서 비동기 메서드를 사용할 때 AsNoTracking()을 함께 사용하면 불필요한 변경 추적을 방지하여 성능을 높일 수 있습니다.

public async Task<List<User>> GetUsersAsync()
{
    using var context = new ApplicationDbContext();
    return await context.Users.AsNoTracking().ToListAsync();
}

5. 결론

C#의 비동기 프로그래밍을 올바르게 활용하면 응답성과 성능을 크게 향상시킬 수 있습니다.

중요한 최적화 기법을 정리하면 다음과 같습니다:

  • ConfigureAwait(false)를 활용하여 컨텍스트 전환 비용 줄이기
  • 불필요한 async/await 사용을 피하기
  • Task.WhenAll을 활용하여 병렬 작업 실행
  • ValueTask를 이용하여 메모리 할당 최소화
  • Parallel.ForEachAsync를 사용하여 대량 데이터 병렬 처리

이러한 기법을 적절히 적용하면 더욱 효율적인 C# 애플리케이션을 개발할 수 있습니다.