await -> try catch

황현중·2025년 12월 9일

1. 한 줄 정리부터

await로 기다리는 비동기 메서드에서 예외가 발생하면,
그 예외는 await가 있는 줄에서 터지는 것처럼 동작하고,
그 주변의 try / catch로 잡을 수 있다.

즉, “비동기라서 예외 처리가 완전 다르다”가 아니라,
“예외가 await 줄에서 터진다고 생각하면 된다” 정도로 이해하면 편하다.


2. 동기 코드 vs 비동기 코드 비교

2-1. 동기 코드에서의 try / catch

try
{
    DoSomething();  // 여기서 예외 나면 바로 catch로 감
}
catch (Exception ex)
{
    Console.WriteLine("에러 발생: " + ex.Message);
}

DoSomething() 안에서 예외가 발생하면,
바로 위의 try / catch 블록으로 예외가 전파된다.

2-2. 비동기 코드에서의 await + try / catch

try
{
    await DoSomethingAsync();  // 이 줄에서 Task가 완료될 때 예외가 있으면 여기서 throw
}
catch (Exception ex)
{
    Console.WriteLine("에러 발생: " + ex.Message);
}

비동기 메서드 안에서 예외가 나면:

  1. 예외가 바로 밖으로 튀어나오는 게 아니라, Task 안에 저장된다.
  2. 나중에 그 Task를 await할 때, 그 줄에서 예외를 다시 던진다(throw).
  3. 그래서 await를 둘러싼 try / catch에서 예외를 잡을 수 있다.

결국 “예외는 await 줄에서 터진다”고 보면 된다.


3. 기본 예제로 흐름 이해하기

static async Task Main()
{
    try
    {
        Console.WriteLine("요청 시작");
        string result = await DownloadDataAsync();  // 여기에서 예외가 다시 throw 됨
        Console.WriteLine("결과: " + result);
    }
    catch (Exception ex)
    {
        Console.WriteLine("예외 잡음: " + ex.Message);
    }

    Console.WriteLine("메인 계속 실행");
}

static async Task<string> DownloadDataAsync()
{
    // 예를 들어 HttpClient 호출 같은 비동기 작업이라고 가정
    await Task.Delay(1000); // 네트워크 대기라고 생각

    // 일부러 에러 발생
    throw new InvalidOperationException("다운로드 중 오류 발생!");
}

실행 흐름을 찬찬히 따라가면:

  1. DownloadDataAsync()를 호출 → Task<string>를 반환
  2. await가 이 Task가 끝날 때까지 기다린다.
  3. DownloadDataAsync 내부에서 throw 발생.
  4. 예외 정보가 Task 안에 저장된다.
  5. Task 완료 후 await 지점으로 돌아올 때, 그 예외를 다시 던진다.
  6. 바깥의 try / catch에서 이 예외를 잡는다.

그래서 “예외는 DownloadDataAsync 안에서 났지만,
실제로 잡는 위치는 await DownloadDataAsync() 줄의 catch라고 보면 된다.


4. 예외를 어디서 잡을지에 따른 패턴

4-1. async 메서드 안에서 직접 처리

static async Task DownloadAndPrintAsync()
{
    try
    {
        string result = await DownloadDataAsync();
        Console.WriteLine("결과: " + result);
    }
    catch (Exception ex)
    {
        Console.WriteLine("DownloadAndPrintAsync 내부에서 처리: " + ex.Message);
    }
}
  • 이 경우, 예외는 DownloadAndPrintAsync 안에서 끝까지 처리된다.
  • 호출하는 쪽에서는 예외가 올라오지 않는다.

4-2. 호출하는 쪽(상위)에서 처리

static async Task DownloadAndPrintAsync()
{
    // 여기서는 예외를 처리하지 않고 위로 올림
    string result = await DownloadDataAsync(); 
    Console.WriteLine("결과: " + result);
}

static async Task Main()
{
    try
    {
        await DownloadAndPrintAsync();
    }
    catch (Exception ex)
    {
        Console.WriteLine("Main에서 예외 처리: " + ex.Message);
    }
}
  • 여기서는 DownloadAndPrintAsync 안에서 예외 처리 없이 그대로 던진다.
  • 최상위 (Main 또는 UI 레이어)에서 한 번에 예외를 처리하는 구조.

결국 “예외를 어디 레벨에서 책임지고 처리할 것인가?”를 설계하는 문제다.


5. await를 안 쓰면 어떻게 되나? (중요 포인트)

// ❌ 이렇게 하면 예외가 try/catch에서 안 잡힐 수 있음
try
{
    Task t = DoSomethingAsync();  // Task만 만든 상태, await 안 함
    // 다른 작업...
}
catch (Exception ex)
{
    Console.WriteLine("여기선 예외 못 잡음");
}
  • await를 하지 않으면, 예외가 **Task 안에서만** 발생하고 호출 스택으로 올라오지 않는다.
  • 나중에 t.Wait()t.Result를 사용할 때 예외가 튀어나오는데,
    이때는 AggregateException 같은 형태로 감싸져서 나와서 더 복잡해진다.

그래서 “비동기 예외를 try / catch로 제대로 처리하고 싶으면 반드시 await 해야 한다”라고 생각하면 편하다.


6. 여러 비동기 작업을 동시에 기다릴 때 (Task.WhenAll)

여러 개 Task를 동시에 돌려놓고 한 번에 기다릴 때도 패턴은 똑같다.

try
{
    Task t1 = DoWorkAsync(1);
    Task t2 = DoWorkAsync(2);
    Task t3 = DoWorkAsync(3);

    await Task.WhenAll(t1, t2, t3);  // 여기서 여러 Task 예외가 한 번에 표출
}
catch (Exception ex)
{
    Console.WriteLine("WhenAll에서 예외 잡음: " + ex.Message);
}
  • Task.WhenAll에 들어간 Task 중 하나라도 예외가 나면,
  • await Task.WhenAll(...) 줄에서 예외가 터진다.
  • 역시 그 줄을 try / catch로 감싸면 예외를 잡을 수 있다.
  • 실제로는 여러 예외가 한 번에 발생할 수도 있어서 내부적으로 AggregateException에 담기기도 한다.
    처음에는 “여러 개 Task → WhenAll에서 한 번에 터진다” 정도만 기억해 두면 충분하다.

7. 자주 쓰는 패턴 정리

패턴 1) async 메서드 안에서 try / catch

static async Task DoSomethingSafeAsync()
{
    try
    {
        await DoSomethingDangerousAsync();
    }
    catch (Exception ex)
    {
        // 여기서 로그 찍고 끝내기
        Console.WriteLine("내부 처리: " + ex.Message);
    }
}

패턴 2) async 메서드는 그냥 던지고, 호출자에서 처리

static async Task DoSomethingAsync()
{
    await DoSomethingDangerousAsync(); // 예외를 위로 던짐
}

static async Task Main()
{
    try
    {
        await DoSomethingAsync();
    }
    catch (Exception ex)
    {
        Console.WriteLine("최상위에서 처리: " + ex.Message);
    }
}

상황에 따라 “어디서 예외를 책임질지”를 선택해서 이 두 패턴을 섞어 쓰게 된다.


0개의 댓글