[C#] 비동기와 코루틴 - async, await

Jinho Lee·2025년 1월 2일
0

개요

  • 유니티 환경에서, Monobehaviour를 상속 받지 않는 클래스에서 비동기적 혹은 코루틴과 유사하게 실행되는 함수를 만드는 방법을 찾는 과정에서 배운 내용을 기술한다.

비동기 프로그래밍이란?

  • 프로그램의 실행을 비동기적으로 처리하는, 즉 하나의 작업이 완료될 때까지 기다리지 않고 다른 작업을 수행할 수 있도록 하는 프로그래밍 방식이다.

  • 아침 식사로 예를 들자면,

     1. 커피 한 잔을 따릅니다.
      2. 팬을 가열한 다음 계란 두 개를 볶습니다.
      3. 베이컨 세 조각을 튀깁니다.
      4. 빵 두 조각을 굽습니다.
      5. 토스트에 버터와 잼을 바릅니다.
      6. 오렌지 주스 한잔을 따릅니다.

    라는 6단계를, 시간이 많이 걸리는 가열 작업들을 동시에 처리하는 것으로

     1. 커피 한 잔을 따릅니다.
      2. 팬을 가열한 다음 계란 두 개를 볶습니다. && 베이컨 세 조각을 같이 튀깁니다. && 빵 두 조각을 같이 굽습니다.
      3. 토스트에 버터와 잼을 바릅니다.
      4. 오렌지 주스 한잔을 따릅니다.

    와 같이 단축할 수 있다.

  • 이처럼 시간이 오래 걸리는 작업 때문에 프로그램이 멈추는 병목 현상을 피하고 작업 시간을 줄이기 위해 비동기 프로그래밍은 사용된다.

  • 특히, 작업이 언제 마무리될지 시간을 가늠하기 어렵고, 그 시간 동안 프로세스가 멈추어선 안되는 작업에 많이 사용된다.

  • 즉 다음과 같은 장점이 있어 비동기는 활용된다.

    1. 자원을 효율적으로 활용할 수 있다.

    2. 메인 스레드를 막지 않고 긴 작업을 수행할 수 있다.

C# 비동기 프로그래밍

Async/Await

  • C# 비동기 프로그래밍의 대표적인 구조이다.

  • async 키워드를 붙인 메소드가 await으로 비동기 작업의 완료 시점까지 대기한다.

  • Task 또는 Task<T>를 반환형으로 사용하며, 내부에서 await 키워드를 통해 실제로 비동기 메서드를 호출한다.

예시

  • 동기 코드

    static void Main(string[] args)
    {
        Coffee cup = PourCoffee();
        Console.WriteLine("coffee is ready");
    
        Egg eggs = FryEggs(2);
        Console.WriteLine("eggs are ready");
    
        Bacon bacon = FryBacon(3);
        Console.WriteLine("bacon is ready");
    
        Toast toast = ToastBread(2);
        ApplyButter(toast);
        ApplyJam(toast);
        Console.WriteLine("toast is ready");
    
        Juice oj = PourOJ();
        Console.WriteLine("oj is ready");
        Console.WriteLine("Breakfast is ready!");
    }
  • 비동기 코드

    static async Task Main(string[] args)
    {
        Coffee cup = PourCoffee();
        Console.WriteLine("coffee is ready");
    
        Egg eggs = await FryEggsAsync(2);
        Console.WriteLine("eggs are ready");
    
        Bacon bacon = await FryBaconAsync(3);
        Console.WriteLine("bacon is ready");
    
        Toast toast = await ToastBreadAsync(2);
        ApplyButter(toast);
        ApplyJam(toast);
        Console.WriteLine("toast is ready");
    
        Juice oj = PourOJ();
        Console.WriteLine("oj is ready");
        Console.WriteLine("Breakfast is ready!");
    }
    • async가 붙은 각 작업(FryEggsAsync 등)이 실행되는 동안 스레드가 차단되지 않고, 작업이 완료되면 이어서 계속하게 된다.

Tack와 Task-based Asynchronous Pattern(TAP)

  • .NET에서 권장하는 비동기 설계 방식으로, Task 객체를 통해 비동기 작업을 표현한다.

  • TaskCompletionSourceCancellationToken 등을 함께 사용해 작업 완료, 취소, 예외 처리를 체계적으로 관리할 수 있다.

  • 후에 더 자세히 조사해 내용을 추가하자.

비동기 함수 구현의 흐름

1. async와 비동기 메소드 정의

  • async 키워드를 사용해 메소드를 정의하는 것으로 해당 메소드가 비동기적으로 동작함을 선언한다.

  • 반환형은 주로 Task 혹은 Task<T>를 사용하며, 경우에 따라 ValueTask 또는 void가 될 수 있으나, void는 이벤트 핸들러 등 특수한 경우에만 권장된다.

  • 일반적인 형태

    public async Task MyAsyncMethod()
    {
        // 비동기로 처리할 로직
    }

2. await와 실제 비동기 작업

  • await 키워드로 비동기 작업을 호출하고, 그 완료 시점을 자연스럽게 기다릴 수 있다.

  • await 키워드가 붙은 작업은 호출할 때 스레드를 블로킹하지 않고 별개 스레드에 비동기적으로 진행된다. 작업이 끝날 때까지 다른 스레드가 정상적으로 동작한다.

  • 일반적인 형태

    public async Task<string> DownloadDataAsync(string url)
    {
        using (var client = new HttpClient())
        {
            // 실제 비동기 메서드 (GetStringAsync)는 Task<string>을 반환
            // await를 붙여서 비동기 완료 시점을 기다린다
            string data = await client.GetStringAsync(url);
            return data; // 작업 완료 후 결과 반환
        }
    }

3. 메소드의 동작 방식 : 상태 기계(State Machine)

  • async/await 패턴을 사용하면, 컴파일러가 자동으로 메서드를 “상태 기계(State Machine)” 형태로 변환한다.

    • 이는 await 키워드로 실행 흐름이 일시 정지된 경우, 함수 내부에 여러 개의 중단점을 설정하고 어떤 단계에서 멈췄는지를 기억해야 한다.

    • C# 컴파일러는 이러한 처리를 위해 메소드를 "상태 기계" 형태로 변환하여, 각 await 지점마다 메소드의 실행 상태(현재 위치, 지역 변수, 예외 처리 상태 등)를 보관한다.

    • async/await 뿐만 아니라 C#의 코루틴 같은 yield return(이터레이터) 문법도 유사한 방식으로 구현된다.

  • 호출 측에서는 메서드가 즉시 Task(또는 Task<T>)를 반환하므로, 스레드가 블로킹되지 않고 비동기 실행이 진행된다.

  • await 키워드 기준으로 코드가 분할되어, 비동기 작업이 완료되면 메서드가 중단된 지점부터 다시 실행을 이어간다.

4. 호출 측에서의 사용 예시

  • 예시 코드

    public async Task ProcessDataAsync()
    {
        // 비동기 메서드 호출
        string result = await DownloadDataAsync("http://example.com");
        Console.WriteLine(result);
    
        // 다운로드 후, 추가 로직 수행
        // (다운로드가 끝날 때까지 메서드의 나머지 부분이 자동으로 대기)
    }
  • 위 코드에서 ProcessDataAsyncDownloadDataAsync가 끝날 때까지 프로그램이 멈추지 않고 다른 작업을 수행할 수 있다.

  • await 이후 로직(출력 등)은 다운로드가 완료된 후에 실행된다.

5. 예외 처리와 흐름 제어

  • 예외 처리

    • await 구문 내부에서 발생하는 예외는 try/catch 구문으로 일반적으로 잡을 수 있다.

    • 예시

      public async Task SafeDownloadAsync()
      {
          try
          {
              string data = await DownloadDataAsync("http://example.com");
              // 성공 시 처리
          }
          catch (Exception ex)
          {
              // 예외 처리
          }
      }
  • 취소(CancellationToken)

    • 긴 작업 중의 취소를 위해 메소드에서 Cancellation 파라미터를 받고, 비동기 작업 도중에 token.ThrowIfCancellationRequested() 등을 적절히 호출해줄 수 있다.

6. 반환형 선택 가이드

  1. Task : 결과값이 없는 단순 비동기 작업

  2. Task<T> : 비동기 처리 후 결과값이 있는 작업

  3. ValueTask / ValueTask<T> : .NET Core 2.1 이상부터 지원하는 반환형으로, 오버헤드가 큰 경우 성능 최적화를 위해 사용할 수 있으나, 사용 시 주의사항이 많다.

  4. void : 이벤트 핸들러 등의 특수 상황에서만 사용하도록 한다. 예외 전파가 어렵고 호출 측에서 작업 완료나 예외를 추적하기 불편하므로 일반 메소드 구현에는 지양하자.

    • 예외 전파 : 상위 계층으로 예외가 전달될때마다 새로운 예외에 포함시켜 다시 던지는 과정.
      예외 체이닝, 예외 래핑이라고 불리기도 한다

7. 실제 사용 예시

/// <summary>
/// [250102 Jinho]
/// 해당 기기가 갖는 기능 권한 내용을 설정
/// 비동기적으로, 0.5초씩 딜레이를 넣어 권한을 매핑
/// </summary>
private async void SetPermission()
{
	if (permissions == null)
	{
    	permissions = new Dictionary<PermissionType, bool>();
    }

	foreach (PermissionType p in System.Enum.GetValues(typeof(PermissionType)))
    {
    	await Task.Delay(500);

		permissions[p] = GetPermissionQuery(p);
        UnityEngine.Debug.Log("permission:" + p);
    }
}
/// <summary>
/// 프레임 큐에서 프레임을 가져와 FFmpeg에 인코딩하도록 전달하는 메서드입니다.
/// </summary>
/// <param name="frameQueue">인코딩할 프레임 큐입니다.</param>
/// <param name="ffmpegStream">FFmpeg의 표준 입력 스트림입니다.</param>
/// <param name="queueLock">프레임 큐에 대한 동기화 잠금 객체입니다.</param>
/// <remarks>
/// 이 메서드는 큐에서 프레임을 가져와 FFmpeg로 전달하며, 녹화가 종료될 때까지 계속 실행됩니다. 
/// 녹화가 종료되면 FFmpeg 스트림을 닫습니다.
/// </remarks>
private async void EncodeFrames(Queue<byte[]> frameQueue, Stream ffmpegStream, object queueLock)
{
	while (isRecording || frameQueue.Count > 0)
    {
    	byte[] frame = null;
        lock (queueLock)
        {
        	if (frameQueue.Count > 0)
            frame = frameQueue.Dequeue();
        }

		if (frame != null)
        {
        	await ffmpegStream.WriteAsync(frame, 0, frame.Length);
            await ffmpegStream.FlushAsync();
        }
        else
        {
        	await Task.Delay(1);
        }
    }
    ffmpegStream.Close();
}

비동기 지연(Delay) 구현

  • Thread.Sleep(밀리세컨드)
    호출한 스레드를 지정된 시간만큼 정지시킨다.
    UI 스레드에서 호출하면 화면과 이벤트 처리가 멈춘다.
    일반적인 UI 스레드에서 권장되지 않는다.

  • Task.Delay(밀리세컨드)
    지정된 시간 동안 비동기로 대기하고, 그동안 스레드를 정지하지 않는다.
    async/await 키워드와 같이 사용하면 UI 프리즈 없이 자연스럽게 지연된다.

  • DispatcherTimer, System.TimeSpan, System.Timers.Timer 등 주기적 호출을 사용한 지연 및 시간 재기도 가능하다.

예시

///// timeDelay01 - All Stop 멈춤
private void btn01_Click(object sender, RoutedEventArgs e)
{
    lbl01.Content = DateTime.Now.ToString("G") + " StartTime \r\n";
    Thread.Sleep(3000);   // 1000은 1초
    lbl01.Content += DateTime.Now.ToString("G") + " EndTime \r\n";
}
 
 
///// timeDelay02 - 창은 멈추지 않으나 마우스로 창 이동중에는 시간이 흐르지 않음
void timeDelay02(int tDelaySecond)
{
    DateTime dtStart = DateTime.Now;
    TimeSpan firstTime = new TimeSpan(DateTime.Now.Ticks);
 
 
    while (firstTime.Ticks + (tDelaySecond * 10000000) >= DateTime.Now.Ticks)
    {
        TimeSpan elapsedSpan = new TimeSpan(DateTime.Now.Ticks - firstTime.Ticks);
        lbl02.Content =
            dtStart + " StartTime \r\n"
            + dtStart.AddSeconds(tDelaySecond) + " EndTime \r\n"
            + elapsedSpan.Seconds.ToString();
 
        this.Dispatcher.Invoke((ThreadStart)(() => { }), DispatcherPriority.Input);
    }
}
 
private void btn02_Click(object sender, RoutedEventArgs e)
{
    timeDelay02(3);    // Second
    MessageBox.Show("The End");
}
 
///// timeDelay03 - 백그라운드에서 시간을 재는 가장 단순하고 좋은 방법
async private void timeDelay03(int tDelaySecond)
{
    await Task.Delay(tDelaySecond);
 
    lbl03.Content += DateTime.Now.ToString("G") + " EndTime \r\n";
    MessageBox.Show("Completed");
}
 
private void btn03_Click(object sender, RoutedEventArgs e)
{
    lbl03.Content = DateTime.Now.ToString("G") + " StartTime \r\n";
    timeDelay03(1000 * 3);    // 1000 = 1 Second
 
    MessageBox.Show("The End");
}
 
///// timeDelay04 - 백그라운드에서 시간을 재는 또다른 방법
private void btn04_Click(object sender, RoutedEventArgs e)
{
    lbl04.Content = DateTime.Now.ToString("G") + " StartTime \r\n";
    DispatcherTimer timer = new DispatcherTimer();
    timer.Interval = TimeSpan.FromSeconds(3);
    timer.Tick += (s, a) =>
    {
        lbl04.Content += DateTime.Now.ToString("G") + " EndTime \r\n";
        MessageBox.Show("Completed");
        timer.Stop();
    };
    timer.Start();
    MessageBox.Show("The End");
}

비동기와 코루틴(Coroutine)

코루틴

  • 유니티에서의 로직은 라이프 사이클처럼 보통 동기 방식으로 돌아간다. 그 사이에 흐름이나 시간을 조절하는 등 비동기적인 작업은 코루틴을 사용해 구현된다.

  • 코루틴은 메소드의 호출과 반환이 같은 프레임에 완료되어야 하는 일반적인 메소드들과 달리, 작업을 여러 프레임에 분산시킬 수 있는 Unity에서 제공하는 메소드이다.

  • Unity에서 코루틴은 실행을 일시 중지(yield)하고 Unity에 제어권을 반환한 다음, 다음 프레임에서 중단된 부분(yield)부터 계속할 수 있는 메소드이다.

    • yield : yield를 사용하여 메소드의 호출자에게 제어권을 양보할 수 있다.
  • 다음과 같은 경우에 코루틴은 사용된다.

    1. 여러 프레임 단위에 걸쳐 처리해야하는 작업을 해야하는 경우

    2. 한 프레임 단위에서 처리하기에는 작업이 너무 오래 걸리는 경우

코루틴과 비동기(Async/Await)

  • 코루틴은 함수의 실행 흐름 자체를 제어하며, 작업을 멈췄다가 계속하는 방식으로, 결국 Main Thread에서 모든 작업이 이루어지기에, 비동기적인 방식이지만 멀티 스레드는 아니다.

  • Async/Awaitawait 키워드로 표시되는 비동기 작업 완료 시점에 그 스레드로 돌아와 코드를 이어 실행하는 방식으로, 복수의 스레드를 사용하는 비동기적 방식이다.

비동기 프로그래밍의 주의사항

데드락(Deadlock)과 레이스 컨디션(Race Condition)

  • 두 개 이상의 스레드가 공유 데이터에 접근하는 과정에서 데드락, 레이스 컨디션 같은 동기화 문제가 발생할 수 있다.

  • 이를 락(Lock), 뮤텍스(Mutex)세마포어(Semaphore) 등의 동기화 수단을 사용하거나, 애초에 불변하도록 구조를 짜는 것으로 방지해야 한다.

예외 처리

  • 비동기 메소드의 예외 흐름

    • try/catch 구문 안에서 await 키워드를 사용하면, 해당 비동기 호출에서 발생하는 예외도 catch 블록에서 잡을 수 있다.

    • 비동기 메소드 내에서 throw된 예외가 최상위까지 전파될 경우, Task.Exception 또는 AggregateException 형태로 포착된다.

  • 비동기 이벤트 핸들러

    • WPF/WinForms 등의 이벤트 핸들러에서 async void 형태로 사용할 때, 예외 처리가 누락되기 쉬우므로 주의한다.

    • 가급적이면 이벤트 핸들러를 async Task 형태로 사용하거나, DispatcherUnhandledException 등의 글로벌 핸들러로 감싼다.

취소와 타임아웃

  • CancellationToken

    • 긴 시간 소요 작업(예: 네트워크 요청, 대용량 파일 처리 등)에서는 취소 기능이 중요하다.

    • 메소드를 async로 만들 때 CancellationToken 매개변수를 받아, 도중에 작업을 중단할 수 있도록 구현한다.

  • 타임아웃

    • HttpClient 등 외부 I/O 호출 시 타임아웃을 설정해 무한 대기를 방지한다.

    • 필요에 따라 Task.WhenAny 등을 사용해 일정 시간 내 작업 미완료 시 다른 로직으로 진행하는 패턴을 사용할 수 있다.

성능 최적화와 모니터링

  • 초당 수천 건 이상으로 호출 빈도가 매우 많으면 오히려 오버헤드가 커져, 자원 관리에 손해를 볼 수 있다.
  • 비동기 디버깅은 StackTrace가 복잡해질 수 있다. 이에 대비해 성능 모니터링 도구(Profiling)와 로깅 체계를 잘 갖춰야 문제점을 빨리 파악할 수 있다.

코루틴/비동기 프로그래밍 베스트 프랙티스

비동기 API 설계 원칙

async 메서드 네이밍 가이드

  • async 메소드 이름에는 "Async" 접미사를 붙이는 것이 관례이다. 반환형은 보통 TaskTask<T>로 한다.

  • 여러 단계의 비동기 호출이 연쇄적으로 이어질 경우, 작업 단위를 명확히 구분하여 재활용 가능하게 구성한다.

Cancellation Token 활용

  1. 간단한 작업에서의 CancellationToken 사용 예시

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    public class CancellationExample
    {
        public async Task DoWorkAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine("작업 시작...");
    
            for (int i = 0; i < 10; i++)
            {
                // 토큰이 취소 상태인지 확인
                if (cancellationToken.IsCancellationRequested)
                {
                    Console.WriteLine("작업 취소 요청됨.");
                    // 필요한 정리 로직이 있다면 추가
                    return; 
                }
    
                // 시뮬레이션용 비동기 대기
                await Task.Delay(500); 
                Console.WriteLine($"작업 진행 중... {i + 1}/10");
            }
    
            Console.WriteLine("작업 완료!");
        }
    }
    • 위 예시 코드에서는 for 루프를 돌며 취소 요청을 계속 확인(cancellationToken.IsCancellationRequested)하고, 취소되면 즉시 메서드를 종료한다.
  2. HttpClient에서 취소 토큰 사용 예시

    using System;
    using System.Net.Http;
    using System.Threading;
    using System.Threading.Tasks;
    
    public class HttpDownloadExample
    {
        private static readonly HttpClient _client = new HttpClient();
    
        public async Task<string> DownloadStringWithCancellationAsync(string url, CancellationToken cancellationToken)
        {
            // HttpClient의 GetAsync, GetStringAsync 등은 CancellationToken을 받는 오버로드가 있음.
            HttpResponseMessage response = await _client.GetAsync(url, cancellationToken);
    
            // 응답 상태코드가 성공이 아니면 예외 발생
            response.EnsureSuccessStatusCode();
    
            // 응답 본문을 문자열로 읽음
            string content = await response.Content.ReadAsStringAsync(cancellationToken);
            return content;
        }
    }
    • HttpClientGetAsync, ReadAsStringAsync 메소드는 취소 토큰을 인자로 받아서, 네트워크 작업 도중에 토큰이 취소 신호를 감지하면 OperationCanceledException 예외를 던진다.
    • 호출 측에서는 이 예외를 잡아서 필요한 처리(로그, 정리 등)를 수행할 수 있다.
  3. 호출부에서의 사용 예시

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    public class Caller
    {
        public async Task RunAsync()
        {
            using var cts = new CancellationTokenSource();
    
            // 지정된 시간 후 자동 취소: 3초 뒤 취소
            cts.CancelAfter(TimeSpan.FromSeconds(3)); 
    
            var worker = new CancellationExample();
            try
            {
                // 비동기 작업에 취소 토큰 전달
                await worker.DoWorkAsync(cts.Token); 
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine("비동기 작업이 취소되었습니다.");
            }
        }
    }
    • CancellationTokenSource : 취소 요청을 발생시키기 위한 객체

      • CancelAfter(TimeSpan)으로 일정 시간 이후 자동으로 Cancel()을 호출해줄 수 있다.
    • cts.Token : 실제 작업에 넘길 취소 토큰(CancellationToken)으로, 작업 내부에서 IsCancellationRequested 확인 혹은 API에서 직접 지원하는 방식으로 사용한다.

    • 취소가 발생하면 OperationCanceledException이 발생하고, 이를 try/catch로 처리할 수 있다.

코루틴 사용 시 주의사항

  • Unity에서의 코루틴

    • IEnumerator 형태의 메소드에 yield return으로 프레임 단위나 특정 조건에서 실행을 제어한다.

    • 코루틴은 Unity 메인 스레드를 기반으로 동작하므로, 무거운 작업(네트워크, 대용량 처리 등)은 코루틴에서 직접 돌기보단 async/await 또는 별도 쓰레드를 사용하는 것이 낫다.

  • 상태 제어

    • 코루틴은 Pause/Resume 제어가 쉽지만, 중단되는 지점이 많으면 프로그램 흐름이 복잡해진다. 각 코루틴의 목적과 중단 조건을 명확히 설계한다.
  • 코루틴 정리

    • 스크립트나 오브젝트가 파괴(Destroy)되면, 해당 오브젝트의 코루틴도 중단해야 메모리 누수나 예기치 않은 로직 실행을 막을 수 있다.

비동기와 코루틴 혼용 전략

  • 코루틴은 스케줄링, 비동기는 I/O

    • 코루틴은 게임 루프나 특정 타이밍 제어(프레임에서 매번 조금씩 작업)를 다루기 좋다.

    • 비동기는 I/O 작업(HTTP 요청, DB 쿼리, 파일 I/O 등)을 다루기 좋다.

    • 두 기법을 필요에 따라 적절히 사용하되, 각각의 책임 범위를 분명히 나누어야 한다.

  • 에러 핸들링 일관성

    • Unity 코루틴에서 예외가 발생하면 별도 로깅이 없으면 알기 어려울 수 있다.

    • async 메소드와 결합된 경우, 예외 처리를 코루틴 내부와 비동기 메소드 내부 어디서 할지 미리 정해둔다.

코드 품질 개선

  • LINQ, Rx, UniTask 등 활용

    • Unity 환경에서 코루틴과 async/await을 쉽게 혼합할 수 있도록 도와주는 UniTask와 같은 라이브러리를 사용하면 코드를 단순화할 수 있다.

    • Reactive Extensions(Rx)를 통해 이벤트 기반으로 비동기 흐름을 선언형으로 작성할 수도 있다.

유지보수 전략

  • 단일 책임 원칙(SRP)

    • 하나의 비동기 메서드가 너무 많은 일을 하지 않도록 분리한다. 유지보수성과 재사용성을 높이는 핵심.
  • 추상화(Abstraction)와 테스트

    • 외부 종속(네트워크, 파일 시스템 등)을 인터페이스로 분리하고, 테스트 시 Mock/Stubs 등을 활용하면 비동기 로직도 단위 테스트가 가능하다.
  • 리팩토링 주기

    • 비동기 메서드가 계속 늘어나면, 내부에서 호출 흐름이 복잡해질 수 있다. 일정 주기로 코드를 검토하고, 로직 분산 또는 통합을 해 준다.

정리

  • 비동기 프로그래밍은 긴 시간 소요 작업으로 인해 UI나 다른 작업이 멈추지 않도록 하는 핵심 기술로, C#에서는 async/awaitTask가 있다.

  • 코루틴은 중단과 재개 매커니즘으로, 게임 엔진 또는 특정 시점 제어가 필요한 환경에서 유용하지만, 일반적인 비동기 프로그래밍과는 설계가 다르다.

  • 비동기를 실제로 적용할 때는 UI 스레드를 정지시키지 않는지와 예외 처리, 동기화 문제(데드락, 레이스 컨디션), 성능 모니터링 등에 각별히 주의해야 한다.

  • 비동기 프로그래밍 요약

참고

0개의 댓글

관련 채용 정보