C#

C# 비동기 프로그래밍 : await와 ContinueWith 비교

우준세 2025. 6. 1. 23:07
728x90
반응형

 

 

 

이번 포스팅은 기존 프로젝트를 리팩토링 하면서 알게 된

Task.ContinueWith 메서드를 분석하기 위해 작성했습니다.

 


| 코드 예제

먼저 같은 기능을 async/await 방식과 Task.ContinueWith 방식으로 구현했습니다.

 

| async/await 방식

public async Task<string> GetDataAsync()
{
    var data = await Task.Run(() =>
    {
        Thread.Sleep(1000); // 긴 작업
        return "완료됨";
    });

    return $"결과: {data}";
}

 

| ContinueWith 방식

public Task<string> GetDataWithContinueAsync()
{
    return Task.Run(() =>
    {
        Thread.Sleep(1000); // 긴 작업
        return "완료됨";
    }).ContinueWith(task =>
    {
        return $"결과: {task.Result}";
    });
}

 

위 두 개의 예제 코드는 기능적으로 유사하지만 코드의 구조와 읽기 쉬움에 차이가 있습니다.

 

| 비교: await vs ContinueWith

항목 await ContinueWith
가독성 매우 높음, 동기 코드처럼 읽힘 중첩되기 쉬움, 콜백 지옥 가능성
예외 처리 try-catch로 자연스럽게 처리 .Exception, .IsFaulted로 별도 처리 필요
동기화 컨텍스트 기본적으로 원래 컨텍스트로 복귀 기본적으로 백그라운드 스레드에서 실행
디버깅 흐름 파악이 쉬움 디버깅 시 콜백 지점 추적 어려움
유연성 깔끔한 흐름 제어 병렬 처리 조합 시 유리할 수 있음

 

await와 ContinueWith의 가장 큰 차이점이라 생각되는 것은

예외 처리 부분입니다.


| 예외 처리

| await 예외 처리

try
{
    string result = await GetDataAsync();
    Console.WriteLine(result);
}
catch (Exception ex)
{
    Console.WriteLine($"예외 발생: {ex.Message}");
}

 

| ContinueWith 예외 처리

GetDataWithContinueAsync().ContinueWith(task =>
{
    if (task.IsFaulted)
    {
        Console.WriteLine($"예외 발생: {task.Exception?.GetBaseException().Message}");
    }
    else
    {
        Console.WriteLine(task.Result);
    }
});

 

ContinueWith에서는 task.IsFaulted를 수동으로 확인하고

task.Exception을 통해 예외를 처리해야 하므로 코드가 복잡해집니다.

 

그럼에도 불구하고 ContinueWith을 사용할 때가 있습니다.


| await 장점

 

  • 가독성 우수: 동기 코드처럼 읽히기 때문에 유지보수가 쉽고, 협업 시 이해하기 쉬움.
  • 예외 처리 편리: try-catch를 통해 자연스럽게 비동기 예외를 처리 가능.
  • 동기화 컨텍스트 자동 복귀: UI 스레드에서 호출 시, 자동으로 UI 컨텍스트로 복귀 (Windows Forms, WPF 등에서 유리).
  • 디버깅이 쉬움: 흐름이 선형적이라 디버깅 도구로 추적이 쉬움.
  • 표준 방식: .NET의 최신 비동기 코드 스타일과 일치, 권장 방식.

| ContinueWith 장점

 

  • 제어 흐름 유연: 여러 작업을 조건적으로 연결하거나 복잡한 체인을 구성할 때 유리.
  • 병렬 처리와 조합 용이: Task.WhenAll, Task.WhenAny, ContinueWhenAll 등과 조합하면 높은 유연성을 가짐.
  • 명시적 실행 컨텍스트 지정 가능: TaskScheduler를 통해 명확히 실행 컨텍스트 지정 가능.

| 유의점

 

  • ContinueWith는 기본적으로 현재 동기화 컨텍스트를 무시하고 새 스레드에서 실행됩니다.
    UI 쓰레드에서 UI 작업을 하려면 TaskScheduler.FromCurrentSynchronizationContext()를 명시적으로 사용해야 합니다.
  • await는 기본적으로 호출한 스레드의 컨텍스트를 캡처하고, 해당 컨텍스트에서 이어 실행합니다.
    단, .ConfigureAwait(false)를 사용하면 캡처하지 않습니다.
  • ContinueWith는 복잡한 작업 흐름을 정의하거나 WhenAll, WhenAny 등과 함께 사용할 때 유용할 수 있으나,
    대부분의 경우 await가 더 직관적이고 안전합니다.

 


| 마무리

정리하면서 느꼈지만 그냥 await/async를 쓰는 게 더 좋아 보이네요 ㅎㅎ

 

질문이나 틀린 부분이 있으면 댓글로 남겨주세요!

 

 

 

 

728x90
반응형