[로봇활용_14주차] C# async & await

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

기다림의 미학: async/await

lock은 한 스레드가 일을 마칠 때까지 다른 스레드를 '블로킹(Blocking)'시킵니다.
만약 UI 스레드가 파일을 다운로드하는 동안 '블로킹'된다면 어떻게 될까요?
사용자는 그동안 '응답 없음' 화면만 쳐다봐야 할 겁니다. 최악의 사용자 경험이죠!
이번 글에서는 비효율적인 '기다림'을 우아하고 효율적인 '기다림'으로 바꿔주는
C# 비동기 프로그래밍의 꽃, asyncawait에 대해 알아보겠습니다.

1)동기와 비동기의 차이점

async/await를 이해하려면 먼저 동기와 비동기의 차이를 알아야 합니다.

비유: 주문 방식

  • 동기 방식(Synchronous):
    손님 한 명이 주문하고, 주문이 나올 때까지 계산대 앞에서 계속 기다립니다.
    그동안 점원은 다른 손님의 주문을 받지 못합니다.
    이것이 블로킹(Blocking) 방식입니다.
  • 비동기 방식(Asynchronous):
    손님이 주문하면 점원은 '진동벨'을 줍니다. 손님은 진동벨을 받고 자리로 갑니다.
    그동안 손님은 친구와 대화하거나 스마트폰을 볼 수 있습니다.
    점원은 손님을 기다리게 하지 않고 바로 다음 손님의 주문을 받을 수 있죠.
    이것이 논블로킹(Non-Blocking) 방식입니다.

async/await는 바로 이 '진동벨' 시스템을 코드에 적용한 것으로 생각하면 쉽습니다!

2)마법의 키워드: asyncawait

에이싱크(async)와 어웨이트(await)는 항상 짝처럼 함께 다닙니다.
이 둘이 어떻게 작동하는지 알아볼까요?

1. async - 비동기 작업 포함

async는 메서드 시그니처 앞에 붙이는 한정자입니다.
이 키워드를 붙이면 컴파일러에 다음과 같이 알려주는 것과 같습니다.

"이 메서드 안에는 await키워드가 있을 수 있습니다.
그러니 비동기 처리를 할 준비를 해주세요!"

async자체는 스레드를 새로 만들거나 특별한 일을 하지 않습니다.
그저 '표시'일 뿐입니다. await를 사용하기 위한 입장권 같은 것이죠.

2. await - 진동벨 받고 대기

await는 비동기 프로그래밍의 핵심입니다.
오래 걸릴 수 있는 작업(I/O 바운드 작업 등)에는 await를 붙입니다.

await를 만나면 프로그램은 이렇게 동작합니다.
1. await뒤의 작업을 시작시키라고 요청합니다.
2. 작업을 기다리지 않고, 즉시 메서드의 제어권을 호출자(Caller)에게 반환합니다.
3. 호출자(UI 스레드 등)는 다른 일을 계속 처리할 수 있습니다.
4. 요청했던 작업이 완료되면, await다음 줄부터 코드 실행을 이어갑니다.

덕분에 우리는 '스레드'를 직접 제어할 필요는 없습니다.
이제 더 중요한 비즈니스 로직에 집중할 수 있게 된 것이죠.

3)C# 코드로 보는 마법

3초 후에 간단한 메시지를 표시하는 콘솔 창 UI를 예로 들어보겠습니다.

[코드]

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        while (true)
        {
            Console.Clear();
            Console.WriteLine("--- 콘솔 창 UI ---");
            Console.WriteLine("1. 동기 방식 실행");
            Console.WriteLine("2. 비동기 방식 실행");
            Console.WriteLine("3. 프로그램 종료");
            Console.Write("\n메뉴를 선택하세요: ");

            string? choice = Console.ReadLine();

            switch (choice)
            {
                case "1":
                    RunSyncExample();
                    break;
                case "2":
                    await RunAsyncExample();
                    break;
                case "3":
                    Console.WriteLine("프로그램을 종료합니다.");
                    return;
                default:
                    Console.WriteLine("잘못된 선택입니다. 다시 시도해 주세요.");
                    break;
            }

            Console.WriteLine("\n아무 키나 눌러 메뉴로 돌아가세요...");
            Console.ReadKey();
        }
    }

    /// <summary>
    /// 동기 방식 (UI가 3초간 멈춤)
    /// </summary>
    private static void RunSyncExample()
    {
        Console.WriteLine("\n[동기 작업 시작]");
        Console.WriteLine("UI 스레드를 차단하여 3초간 아무것도 할 수 없습니다.");

        Thread.Sleep(3000); // 3초간 강제 대기

        Console.WriteLine("\n동기 작업 완료!");
    }

    /// <summary>
    /// 비동기 방식 예제 (UI가 멈추지 않음)
    /// </summary>
    private static async Task RunAsyncExample()
    {
        Console.WriteLine("\n[비동기 작업 시작]");
        Console.WriteLine("UI 스레드를 차단하지 않아 다른 작업도 동시에 수행할 수 있습니다.");
        Console.WriteLine("아래에 로딩 애니메이션이 표시됩니다.");

        // 1. 오래 걸리는 작업을 Task로 실행하고 즉시 제어권을 돌려받음
        Task longRunningTask = LongRunningOperationAsync();

        // 2. longRunningTask가 완료될 때까지 다른 작업(로딩 애니메이션)을 수행
        while (!longRunningTask.IsCompleted)
        {
            ShowLoadingAnimation();
            // 애니메이션이 너무 빨리 돌지 않도록 잠시 대기
            // 이 Delay조차도 비동기이므로 스레드를 차단하지 않음
            await Task.Delay(100);
        }

        // 3. 오래 걸리는 작업이 끝나기를 기다림 (이미 끝났으므로 바로 통과)
        await longRunningTask;

        // 커서 위치를 정리하고 완료 메시지 출력
        Console.SetCursorPosition(0, Console.CursorTop);
        Console.Write(new string(' ', 20)); // 애니메이션 흔적 지우기
        Console.SetCursorPosition(0, Console.CursorTop);

        Console.WriteLine("\n비동기 작업 완료!");
    }

    /// <summary>
    /// 3초가 걸리는 비동기 작업을 시뮬레이션하는 메서드
    /// </summary>
    private static async Task LongRunningOperationAsync()
    {
        // await 키워드로 비동기 작업을 기다림
        // Task.Delay는 스레드를 차단하지 않고 일정 시간 기다리는 비동기 작업
        await Task.Delay(3000);
    }

    private static int _spinnerPosition = 0;
    private static readonly char[] _spinnerChars = new char[] { '|', '/', '-', '\\' };

    /// <summary>
    /// 콘솔에 로딩 애니메이션을 표시하는 헬퍼 메서드
    /// </summary>
    private static void ShowLoadingAnimation()
    {
        Console.SetCursorPosition(0, Console.CursorTop);
        Console.Write($"작업 중... {_spinnerChars[_spinnerPosition]}");
        _spinnerPosition = (_spinnerPosition + 1) % _spinnerChars.Length;
    }
}
  • [실행 결과]

async/await를 사용한 비동기 버전에서는 UI가 동작합니다.
UI 스레드가 Task.Delay를 기다리느라 묶여있지 않았기 때문이죠!

4)async/await에 대한 특징

  • async전염 (Async All the Way): await를 사용하려면 메서드는 async여야 합니다.
    그리고 그 메서드를 호출하는 상위 메서드도 처리하려면 asyncawait를 붙여야 하죠.
    보통 호출 스택의 최상단까지 async가 이어지는 경향이 있습니다.
  • async void는 피하세요: async메서드는 보통 Task 또는 Task<T>를 반환해야 합니다.
    async void는 작업이 언제 끝나는지, 성공했는지, 실패했는지 추적하기가 어렵습니다.
    이벤트 핸들러(예: 버튼 클릭)처럼 반환 타입이 정해진 경우에만 예외적으로 사용합니다.
  • async/await는 멀티스레딩이 아니다?: 가장 흔한 오해입니다.
    async/await는 기존 스레드가 놀지 않도록 효율적으로 재사용하는 것에 가깝습니다.
    (CPU 바운드 작업을 비동기로 처리할 때는 Task.Run을 통해 스레드 풀을 사용합니다.)

5)정리

직접 Thread를 만들고 관리하는 것은 복잡하고 유지 보수를 어렵게 만듭니다.
async/await는 복잡한 스레드 관리 없이 코드가 깔끔해지고,
논블로킹(Non-Blocking) 동작을 구현할 수 있게 해주는 C#의 강력한 기능입니다.

구분동기 (Synchronous)비동기 (Asynchronous)
비유계산대 앞에서 계속 기다리기진동벨 받고 다른 일 하기
스레드 동작블로킹(Blocking)논블로킹(Non-Blocking)
결과응답성 저하 (UI 멈춤 등)높은 응답성, 효율적인 자원 사용
profile
🚀 미래의 엔지니어를 꿈꾸는 훈련생의 기록 📝

0개의 댓글