await와 async

황현중·2025년 12월 3일

C#

목록 보기
22/24

1. 한 줄 정의

async

메서드나 람다 선언부에 붙이는 키워드.
“이 메서드는 비동기 스타일로 작성되었고, Task 또는 Task<T>를 반환할 거야” 라는 표시.

await

메서드/람다 몸통 안에서 쓰는 키워드(연산자).
“이 비동기 작업(Task)이 끝날 때까지 비동기적으로 기다렸다가, 끝나면 그 다음 줄부터 이어서 실행해 줘.” 라는 뜻.

2. 어디에 쓰는 키워드인가?

2-1. async는 ‘메서드 선언부’에 붙는다

async Task DoWorkAsync()
{
    // 여기 안에서 await 사용 가능
}

async Task<int> GetNumberAsync()
{
    await Task.Delay(1000);
    return 10;
}
  • async는 메서드 이름 앞에 붙는다.
  • “이 메서드는 비동기 상태머신으로 변환된다”는 신호를 컴파일러에 주는 역할을 한다.
  • 반환 타입은 보통 Task / Task<T> / (특수한 경우) void 중 하나다.

중요한 점 하나: async를 붙였다고 자동으로 새 스레드가 만들어지는 게 아니다.
그냥 “비동기 로직을 담고 있고, 안에서 await 쓸 거야” 정도의 표지판에 가깝다.

2-2. await는 ‘메서드 몸통 안’에서만 쓴다

async Task TestAsync()
{
    await Task.Delay(1000); // OK
}

void Test()
{
    await Task.Delay(1000); // ❌ 컴파일 에러: async 메서드가 아니라서
}
  • await반드시 async가 붙은 메서드/람다 안에서만 사용할 수 있다.
  • await someTask는 “someTask가 끝날 때까지 비동기적으로 기다렸다가, 다시 여기로 돌아와서 아래 코드를 이어서 실행하자” 라는 의미다.
  • Task<T>를 await 하면, T 결과값을 바로 꺼내 쓸 수 있다.
int result = await GetNumberAsync();  // GetNumberAsync가 Task<int>를 반환한다고 가정

위 한 줄은 사실상:

  1. GetNumberAsync()가 시작되고 Task<int> 하나가 만들어진다.
  2. 그 Task가 완료될 때까지 비동기적으로 대기한다.
  3. 완료되면 그 안의 int 값을 꺼내서 result에 넣는다.

3. 역할 관점에서 보는 async vs await

3-1. async의 역할

  • 이 메서드를 비동기 상태머신으로 변환하도록 컴파일러에게 지시한다.
  • 이 안에 await를 쓸 수 있게 해준다.
  • 반환 타입을 Task / Task<T> / void 중 하나로 맞추게 만든다.

흔한 오해 중 하나가:

“async 붙이면 자동으로 다른 스레드에서 실행된다”

이건 아니다. 새 스레드를 만드는 건 Task.Run, Thread, ThreadPool 같은 애들이고, async는 단지 비동기 흐름을 코드 구조로 표현하기 위한 키워드다.

3-2. await의 역할

  • Task 또는 Task<T>완료될 때까지 비동기적으로 대기한다.
  • 대기하는 동안 스레드를 블록시키지 않는다 (UI 프리징 방지의 핵심).
  • 작업이 끝나면 이어서 아래 줄 코드를 실행한다.
  • Task<T>를 await하면, 결과 T를 바로 받을 수 있다.
async Task DemoAsync()
{
    Console.WriteLine("1");

    await Task.Delay(1000); // 1초 후, 다시 여기로 돌아와서

    Console.WriteLine("2"); // 이 줄부터 이어서 실행
}

여기서 await Task.Delay(1000)Thread.Sleep(1000)으로 바꾸면 1초 동안 스레드가 진짜로 멈춰버린다. await는 “기다린다”는 의미는 같지만, 스레드를 점유하지 않는다는 점이 결정적으로 다르다.


4. async와 await의 관계

4-1. async만 있고 await가 없을 때

async Task FooAsync()
{
    Console.WriteLine("Hello");
    // await 없음
}

이런 코드는 사실상 동기 메서드와 거의 같다. 컴파일러가 내부적으로 Task를 감싸긴 하지만, 중간에 “끊기는 지점”이 없기 때문에 흐름은 그냥 위에서 아래로 쭉 실행된다.

그래서 진짜 비동기의 느낌이 나는 지점은 결국 await를 만나는 순간부터라고 보면 된다.

4-2. await만 쓰고 async를 안 쓸 때

void Foo()
{
    await Task.Delay(1000); // ❌ 컴파일 에러
}

이런 코드는 컴파일 자체가 안 된다. await는 반드시 async 메서드 안에서만 사용 가능하기 때문이다.

즉, 둘의 관계를 요약하면:

  • awaitasync 없이 쓸 수 없다.
  • asyncawait 없이 쓸 수는 있지만, 의미가 거의 없다.

5. 간단 예제로 전체 흐름 다시 보기

async Task<int> GetNumberAsync()
{
    await Task.Delay(1000); // 1초짜리 비동기 작업
    return 10;
}

async Task ShowAsync()
{
    Console.WriteLine("시작");

    int n = await GetNumberAsync();   // 여기서 비동기 대기 + 결과 받기

    Console.WriteLine("결과: " + n);
}
  • GetNumberAsync → “나중에 int를 줄 비동기 메서드야” (반환 타입: Task<int>)
  • await Task.Delay(1000) → 1초 뒤에 다시 아래 줄부터 이어서 실행
  • int n = await GetNumberAsync();Task<int>가 끝날 때까지 기다렸다가, 안의 int 값(10)을 꺼내서 n에 대입

6. 한 번에 요약

  • async
    • 메서드/람다 선언부에 붙는다.
    • “이 메서드는 비동기 스타일로 작성됐다, 안에서 await 쓸 거다” 라는 표시.
    • 반환 타입은 Task, Task<T>, void(이벤트 핸들러) 중 하나.
  • await
    • 비동기 작업(Task, Task<T>) 앞에 붙는 연산자.
    • 해당 Task가 완료될 때까지 비동기적으로 기다린 뒤, 다음 줄부터 이어서 실행.
    • Task<T>라면 결과 T를 바로 꺼내서 변수에 넣어줌.

정말 짧게 말하면:

async는 “이 메서드는 비동기 모드로 작성할게” 라고 선언하는 스위치이고,
await는 “이 비동기 작업이 끝날 때까지 잠깐 맡겨 두고, 끝나면 여기서부터 다시 시작하자” 라고 말하는 실행 지점이다.

0개의 댓글