Async/Await in .NET는 제한 없이 사용하면 리소스를 낭비할 수 있어요 🚀🛑

발행: (2025년 12월 28일 오후 01:54 GMT+9)
4 min read
원문: Dev.to

Source: Dev.to

소개

우리 중 많은 사람들은 async/await를 사용하면 애플리케이션이 자동으로 “스케일러블”해진다고 생각합니다. 하지만 제한(throttling) 메커니즘 없이 async 코드를 남발하면, 오히려 서버 자원을 순식간에 소모하는 무기고가 될 수 있습니다. 이 글(및 최신 YouTube 영상)에서는 Task.WhenAll을 제한 없이 사용할 때 왜 위험한지, 그리고 SemaphoreSlim이 어떻게 서버를 구해줄 수 있는지 살펴보겠습니다.

화면 뒤에서 무슨 일이 일어나나요?

  • Socket Exhaustion – 서버가 외부 연결을 위한 포트를 모두 소진합니다.
  • Database Pool Starvation – 모든 DB 연결이 사용 중이라 다른 요청이 대기합니다.
  • RAM Spike – 각 작업마다 자체 상태 머신과 메모리 할당이 발생합니다.

그 결과 서버가 “멈추고”, 요청이 타임아웃되며, 사용자는 실망하게 됩니다.

SemaphoreSlim

SemaphoreSlim은 .NET에서 동시 실행 가능한 작업 수를 제한하는 가장 가벼운 방법입니다.

올바른 구현 방법

// 최대 10개의 동시 작업을 제한
using var semaphore = new SemaphoreSlim(10);

var tasks = dataList.Select(async d =>
{
    await semaphore.WaitAsync(); // 여기서 대기열에 들어감
    try
    {
        await CallExternalApiAsync(d);
    }
    finally
    {
        semaphore.Release(); // 다음 작업을 위해 슬롯 해제
    }
});

await Task.WhenAll(tasks);

이 코드를 사용하면 1,000개의 데이터가 있더라도 동시에 처리되는 데이터는 10개에 불과합니다. 서버 자원은 안전하게 유지되고, 애플리케이션은 여전히 응답성을 유지합니다.

모범 사례

  • try...finally 사용: Release()는 반드시 finally 블록에서 호출하세요. 그렇지 않으면 세마포 슬롯이 “누수”되어 애플리케이션이 데드락에 빠질 수 있습니다.
  • 합리적인 제한값 설정: 무작정 숫자를 정하지 말고, 자원(예: 데이터베이스 풀의 최대 연결 수)에 맞게 제한값을 조정하세요.
  • Semaphore vs Lock: async 코드에서는 일반 lock 사용을 피하세요. lock은 스레드를 차단하지만, WaitAsync()는 대기 중에도 스레드가 다른 작업을 수행하도록 해줍니다.

결론

async/await는 강력한 도구이지만, SemaphoreSlim은 그 강력함을 제어할 수 있게 해줍니다. 동시에 너무 많은 작업을 시도해서 서버가 무너지지 않도록 주의하세요. 적절히 동시성을 제한하면 애플리케이션은 여전히 빠르고 안정적으로 동작합니다.

Back to Blog

관련 글

더 보기 »