비동기 - Task, async, await

황현중·2025년 12월 3일

C#

목록 보기
20/24

1. 동기 vs 비동기부터 직관적으로 이해하기

1-1. 동기(synchronous) – 줄 서서 한 줄씩 처리

동기 코드는 “한 줄이 끝나야 다음 줄로 넘어가는 방식”이다.

Console.WriteLine("1. 시작");

DoWork();   // 여기서 이 일이 끝날 때까지 멈춰 있음

Console.WriteLine("2. 끝");
  • DoWork() 안에서 5초가 걸리면,
  • 그 5초 동안 프로그램은 이 줄에서 멈춰 있다.
  • 그 다음 줄("2. 끝" 출력)은 절대 먼저 실행되지 않는다.

UI 프로그램(WinForms, WPF, 웹 등)에서는 이렇게 오래 걸리는 작업을 동기로 하면, 화면이 멈춘 것처럼 느껴진다.

1-2. 비동기(asynchronous) – “알바에게 시키고 나는 딴 일 계속”

편의점 사장 입장에서 생각해 보자.

  • 손님: “컵라면 뜨거운 물 좀 부어주세요.”
  • 동기 방식이라면?
    • 사장이 컵라면에 물 붓고 라면 익을 때까지 3분 동안 계속 지켜본다.
    • 그동안 다른 손님 계산도 못하고, 가게 전체가 멈춘 느낌이 된다.
  • 비동기 방식이라면?
    • 사장: 알바한테 “저 컵라면 3분 뒤에 완성되면 손님께 드려.” 하고 맡긴다.
    • 그 사이 사장은 다른 손님 계산을 계속 처리한다.
    • 3분 후, 알바가 “컵라면 됐어요!” → 사장이 손님에게 건네준다.

여기서 비유를 맞춰 보면:

  • 알바에게 맡긴 일 하나Task
  • “이거 끝나면 알려줘” 하고 기다리는 지점await
  • “컵라면 끓여줘”라고 정의된 메서드async 메서드

2. 세 키워드 한 줄 정리

  • Task: “미래에 끝날 작업 하나”를 나타내는 타입
  • async: “이 메서드는 비동기 스타일로 작성할 거야”라는 표시
  • await: “이 Task가 끝날 때까지 잠깐 맡겨두고, 끝나면 여기서부터 이어서 실행해줘”

각각을 조금 더 자세히 보자.

2-1. Task – 미래에 끝날 작업(약속)

Task = 아직 안 끝났을 수도 있지만 언젠가는 끝날 ‘작업’ 하나를 나타내는 객체

두 가지 형태가 있다.

  • Task – 리턴값 없는 작업
  • Task<T> – 나중에 T 타입 결과를 돌려주는 작업
Task t = WorkAsync();          // 나중에 끝나는 작업 (리턴값 없음)
Task<int> t2 = CalcAsync();    // 나중에 int 결과를 하나 돌려줌

감각적으로는 “배달 주문 번호”랑 비슷하다.
주문 넣으면 바로 음식이 나오는 게 아니라 주문 번호(Task)를 받고, 음식은 나중에 나온다.

2-2. async – “이 메서드는 비동기 메서드입니다” 표시

비동기 메서드를 만들 때는 이렇게 쓴다.

async Task DoSomethingAsync() { ... }        // 리턴값 없음
async Task<int> GetNumberAsync() { ... }     // int 리턴

async“이 안에서 await를 쓸 거야”라고 표시하는 키워드라고 보면 이해하기 쉽다.
실제 비동기 동작은 await가 담당한다.

2-3. await – “끝나면 다시 여기로 돌아와”

await Task.Delay(2000);

이 한 줄의 의미는:

“2초 뒤에 끝나는 작업을 하나 시작해 놓고,
그 작업이 끝나면 현재 메서드의 다음 줄부터 다시 실행해 줘.”

중요한 점은, 이 기다리는 동안 스레드를 붙잡고 자는 게 아니라는 것.
“2초 뒤에 여기서 다시 이어서 실행해”라고 예약해 놓고, 스레드는 다른 일을 할 수 있게 돌려준다.


3. 가장 단순한 async/await 예제

일단 콘솔 기준으로 제일 짧은 예제를 보자.

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Console.WriteLine("1. 시작");

        await Task.Delay(2000);  // 2초 동안 비동기 지연

        Console.WriteLine("2. 2초 후에 실행");
    }
}

3-1. 실행 흐름 설명

  1. "1. 시작"이 바로 출력된다.
  2. await Task.Delay(2000);를 만난다.
    • “2초 뒤에 다시 여기로 돌아와서 그 다음 줄부터 실행해라”라고 예약.
    • 그 사이 스레드는 놀고 있지 않고, 다른 작업에 쓸 수 있다.
  3. 2초가 지나면, 예약된 대로
    • Console.WriteLine("2. 2초 후에 실행");이 실행된다.

겉으로 보기에는 Thread.Sleep(2000) 과 비슷하게 “2초 후에 다음 줄 실행”처럼 보이지만, 내부는 스레드를 멈추지 않고 비동기로 동작한다는 점이 다르다.


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

4-1. 동기 버전 – Thread.Sleep

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Console.WriteLine("A");

        Thread.Sleep(2000);  // 2초 동안 스레드가 진짜로 멈춰 있음

        Console.WriteLine("B");
    }
}
  • 출력 순서: A → (2초 멈춤) → B
  • 그 2초 동안 이 스레드는 아무 일도 못 한다.
  • UI 스레드에서 이렇게 하면 화면이 “응답 없음”처럼 멈춘다.

4-2. 비동기 버전 – await Task.Delay

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Console.WriteLine("A");

        await Task.Delay(2000);  // 2초 동안 비동기 지연

        Console.WriteLine("B");
    }
}

출력 결과는 똑같이 A → 2초 후 → B 이지만, 내부 동작이 다르다.

  • Thread.Sleep : 스레드가 2초 동안 완전히 쉰다.
  • await Task.Delay :
    • “2초 뒤에 다시 이어서 실행해”라고 예약해놓고,
    • 그동안 스레드는 다른 일을 할 수 있게 풀어준다.

콘솔 앱에서는 체감이 안 될 수 있지만, UI 프로그램(WinForms, WPF, MAUI 등)에서는 이 차이가 “화면이 멈추느냐, 멈추지 않느냐”로 크게 드러난다.


5. 라면 끓이기 예제로 보는 Task + async + await

아까 편의점 비유를 실제 코드로 옮겨 보면 훨씬 이해가 쉽다.

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Console.WriteLine("1. 라면 주문 넣기");

        string result = await CookRamenAsync();  // 라면 끓이는 비동기 메서드 호출

        Console.WriteLine("3. 라면 받음: " + result);
    }

    // 라면 끓이는 비동기 메서드
    static async Task<string> CookRamenAsync()
    {
        Console.WriteLine("2. 라면 끓이는 중... (3초)");

        await Task.Delay(3000);  // 3초 동안 끓이는 중이라고 가정

        return "완성된 라면";
    }
}

5-1. 여기서 중요한 포인트

  • CookRamenAsync의 선언:
    static async Task<string> CookRamenAsync()
    • async → 이 메서드는 비동기 스타일로 동작한다.
    • Task<string> → “나중에 string 결과를 줄 작업”을 나타낸다.
  • <li><code>await CookRamenAsync()</code>의 의미:
      <ul>
        <li><code>CookRamenAsync()</code>를 호출하면 <code>Task&lt;string&gt;</code>이 하나 생긴다.</li>
        <li><code>await</code>는 그 작업이 끝날 때까지 기다렸다가,</li>
        <li>끝나면 그 결과(<code>string</code>)를 꺼내서 <code>result</code>에 넣어준다.</li>
      </ul>
    </li>

정리하면:

  • async Task<T> 메서드 → “나중에 T를 줄게”라고 약속하는 비동기 메서드
  • await → “그 약속(Task<T>)이 끝날 때까지 기다렸다가, T를 꺼내서 계속 진행해 줘”

6. 한 번에 정리해 보기

6-1. 세 키워드 요약

  • Task
    • “미래에 끝날 작업”을 나타내는 타입
    • Task, Task<T> 두 가지가 핵심
  • async
    • “이 메서드는 비동기로 동작할 거야”라는 표시
    • 메서드 리턴 타입을 Task 또는 Task<T>로 만든다.
  • await
    • “이 Task가 끝날 때까지 기다렸다가, 끝나면 다음 줄부터 다시 이어서 실행해 줘.”
    • 스레드를 묶어두지 않고, 다시 사용할 수 있게 반납하는 패턴

6-2. 왜 이렇게 복잡하게 쓸까?

  • 파일 IO, DB 쿼리, 웹 API 요청처럼 시간이 오래 걸리는 작업 때문에 프로그램 전체(특히 UI)가 멈추지 않게 하려고
  • 원래 비동기 코드는 콜백 지옥처럼 복잡해지기 쉬운데, async/await를 쓰면 동기 코드처럼 위에서 아래로 읽히는 구조로 만들 수 있어서 유지보수가 훨씬 쉽다.

0개의 댓글