CodeGym /행동 /C# SELF /블로킹 호출 문제

블로킹 호출 문제

C# SELF
레벨 59 , 레슨 0
사용 가능

1. 도입

블로킹 호출 — 어떤 연산이 완료될 때까지 스레드의 실행을 “막아버리는” 모든 호출입니다. 보통 다음과 같습니다:

  • 데이터 읽기 또는 쓰기(예: 디스크).
  • 오래 걸리는 DB나 네트워크 요청.
  • 느리게 동작하는 써드파티 라이브러리 호출.

이런 순간에는 스레드가 그냥 서서 발만 동동 구르며 기다립니다: “뭐야, 언제 응답이 오냐?”

실생활 예시


// 당신의 주요 로직
Console.WriteLine("서버 응답 대기 중...");
string response = CallServer(); // 여기서 모든 게 멈춤!
Console.WriteLine("서버 응답: " + response);

서버가 응답할 때까지 당신의 스레드는 "멈춤" — 다른 일을 할 수 없습니다!

애플리케이션에서의 모습

애플리케이션 유형에 따라 다르게 드러납니다. 콘솔 프로그램에서는 단순히 “정지”한 것처럼 보입니다 — 커서 깜빡임이 멈추고 반응이 없습니다. WinForms나 WPF 같은 GUI에서는 창이 갑자기 응답을 멈추고 친숙한 “not responding”이 뜨며 사용자가 소프트웨어와 당신을 저주할 준비를 하게 됩니다. 서버 애플리케이션에서는 상황이 더 안 좋아집니다: 하나의 블로킹된 스레드는 곧 한 명의 서비스 가능한 사용자가 줄어드는 의미입니다.

2. C#에서의 블로킹 호출 실제

학습용 프로젝트로 상황을 직접 만져보죠. 마법의 구슬 앱이 있다고 합시다. 예를 들면 다음과 같은 코드가 있을 수 있습니다:


Console.WriteLine("마법의 구슬에 질문을 입력하세요:");
string question = Console.ReadLine(); // 블로킹 호출: 입력을 기다림!
Console.WriteLine("답을 생각 중...");
Thread.Sleep(5000); // 오래 걸리는 작업 에뮬레이션
Console.WriteLine("답: 내일 다시 시도해보세요!");
  • Console.ReadLine() — 필요한 만큼 사용자 입력을 기다립니다(스레드를 블로킹).
  • Thread.Sleep(5000) — 인위적으로 일시정지하여 스레드를 얼립니다.

질문: 콘솔 애플리케이션에서 이게 왜 나쁘죠?

답: 콘솔에서는 그렇게 치명적이지 않습니다 — 사용자는 기다리는 걸 알고 있습니다. 하지만 사용자가 한 명이 아니라면? 1000개의 동시 요청이 들어오는 서버라면? 또는 사용자가 인터페이스의 반응을 기다리는 GUI 앱이라면, 철학적 사색을 하는 동안 인터페이스가 응답하지 않습니다.

예: UI 차단


// WinForms의 버튼 핸들러:
private void button1_Click(object sender, EventArgs e)
{
    label1.Text = "로딩 중...";
    DoHeavyWork(); // 무거운 작업(예: DB 요청)
    label1.Text = "완료!";
}

문제: DoHeavyWork가 실행되는 동안 창은 UI를 갱신하지 못하고 키보드나 마우스에 반응하지 않으며 다시 그려지지 않습니다. 사용자가 창을 닫으려 하면 Windows는 "응답 없음"을 표시하고 작업 종료를 권할 겁니다.

3. 멀티스레드 세계의 큰 골칫거리

블로킹 호출의 문제점

  • 리소스 낭비. .NET의 각 스레드(Thread)는 상당히 "무거운" 객체입니다. OS는 보통 1MB 정도의 스택 메모리, 핸들, 동기화 자료구조 등을 할당합니다. 스레드가 유휴 상태(파일/네트워크 대기)이면 아무 것도 안 하면서 메모리를 먹습니다.
  • 스레드 한계. 서버 앱엔 보통 thread pool(스레드 풀)이 있습니다. 모든 스레드가 블로킹되면 새로운 요청을 처리할 수 없습니다. 예: ASP.NET에서 모든 사용 가능한 스레드가 DB 응답을 기다리면 사이트 전체가 “멈춤” 상태가 됩니다.
  • 나쁜 UI 응답성. GUI 앱에서 UI 스레드가 블로킹되면 창은 닫기 버튼에도 반응하지 않습니다.
  • 성능 저하. 많은 스레드가 블로킹될수록 컨텍스트 스위칭이 증가하고 OS 부하가 커져 전체 시스템이 느려집니다.

블로킹 호출의 전형적 원천

어떤 호출이 스레드를 갑자기 블로킹할 수 있는지 봅시다:

  • 네트워크 연산: API 호출, 파일 업/다운로드, 업데이트 다운로드.
  • 디스크/파일 시스템: 큰 파일 읽기나 쓰기.
  • 데이터베이스: 오래 걸리는 SQL Server 쿼리 혹은 로컬 DB 조회.
  • 사용자 입력: 긴 시간의 ReadLine — 사소해 보여도 블로킹합니다.
  • Thread.Sleep, Task.Delay: 인위적으로 스레드를 “얼려”버립니다.
예시 (웹에서 데이터 로드)

using System.Net.Http;

HttpClient client = new HttpClient();
string result = client.GetStringAsync("https://google.com").Result; // 블로킹 동기 호출!
Console.WriteLine(result);

무슨 일이냐면 .Result가 응답을 받을 때까지 스레드를 블로킹합니다!

4. 호출이 스레드를 블로킹하는지 어떻게 알까?

간단합니다: 메서드가 작업이 끝날 때까지 제어를 반환하지 않으면(대기, 입력, 네트워크, 디스크) — 그건 블로킹 호출입니다.

  • Thread.Sleep, Task.Wait, .Result, .Wait() 같은 메서드들은 블로킹합니다.
  • 비동기적이지 않은 방식으로 데이터 읽기/쓰기가 나타나는 메서드들도 블로킹입니다.
  • “멈춘” 창이나 다운된 서버는 흔한 신호입니다.

시각적 요소: 블로킹 호출의 흐름도


+-------------------+
| 메서드 실행 시작  |
+-------------------+
         |
         v
+-------------------+
| 블로킹 메서드 호출|
| (예: 파일 읽기)   |
+-------------------+
         |
         v
+-------------------+
| 연산 완료까지 대기 |
+-------------------+
         |
         v
+-------------------+
| 메서드 반환 후    |
| 다음으로 진행     |
+-------------------+

5. 서버 애플리케이션에서 블로킹 호출이 왜 나쁜가

현대 애플리케이션의 대부분은 네트워크나 서버 기반입니다. 게임을 만들더라도 서버에서 리소스를 로드하죠. 웹사이트라면 네트워크와 디스크 작업이 확실히 존재합니다.

서버가 초당 1000개의 요청을 처리한다고 상상해보세요. 각 요청이 DB와 통신해야 합니다. 당신은 이렇게 코드를 씁니다:


// 웹 핸들러
string data = db.ReadDataSync(); // 동기 블로킹!
return new Response(data);

DB 요청이 진행되는 동안 해당 스레드는 단순히 놀고 기다립니다. 하나쯤은 괜찮지만 요청이 쌓이면 모든 스레드가 채워집니다. 새 요청은 처리되지 못하고 대기열에 밀려납니다. 결국 서버는 기계라기보다 긴 줄이 된 상태가 됩니다.

예시: "동기식 요청 처리"


요청 1: 스레드 차지 -> DB 대기 -> 해제
요청 2: 스레드 차지 -> DB 대기 -> 해제
...
요청 100: 사용 가능한 스레드 없음, 대기열에...

6. 비동기성 — 블로킹의 해독제

블로킹을 피하려면 비동기 호출을 사용합니다. C#에서는 이게 바로 asyncawait입니다.

이것들은 허용합니다:

  • 스레드(특히 UI나 서버 스레드)를 블로킹하지 않기.
  • 쓸데없는 리소스 점유를 피하기.
  • GUI 응답성 개선.
  • 느린 작업이 진행되는 동안 스레드를 “잡아두지 않기”.

주의: 우리는 곧바로 모든 것을 비동기로 바꾸라고 강요하진 않습니다 — 비동기는 스레드와 블로킹에 대한 이해를 필요로 합니다. 블로킹 호출의 고통을 이해한 후라면 비동기 접근 방식이 정신적으로나 사용자 측면에서 얼마나 쾌적한지 느낄 수 있을 겁니다.

표: 무엇이 블로킹하고 무엇이 아닌가?

메서드/호출 스레드 블로킹 여부 UI/Server에 적합한가
Console.ReadLine()
아니오
Thread.Sleep(1000)
아니오
File.ReadAllText()
아니오
Task.Delay(1000).Wait()
아니오
HttpClient.GetStringAsync().Result
아니오
await File.ReadAllTextAsync()
아니요
await Task.Delay(1000)
아니요
await HttpClient.GetStringAsync()
아니요

참고: await는 오직 async Task ... 같은 비동기 함수 안에서만 동작합니다. 이건 다음 강의에서 자세히 다루겠습니다.

7. 블로킹 호출 작업에서 흔한 실수들

“UI를 블로킹하면 사용자들이 다 싫어해”


private void btnLoad_Click(object sender, EventArgs e)
{
    var data = BigFileReader.Read(@"C:\huge.dat"); // 블로킹 호출!
    textBox1.Text = data;
}

큰 파일이 읽히기 전까지 창이 “멈춰버립니다”.

“서버가 멈추면 클라이언트는 도망간다”


public IActionResult Download()
{
    var content = File.ReadAllBytes("bigfile.zip"); // 동기적이고 오래 걸리며 ASP.NET 스레드를 블로킹!
    return File(content, "application/zip");
}

작은 서버(예: 무료 호스팅)에서는 이런 접근으로 쉽게 “다운”될 수 있습니다.

“비동기 메서드 + .Wait()/.Result = 동기적 죽음”


public void LoadData()
{
    var result = DoAsyncWork().Result; // 스레드를 블로킹!
}

.Result.Wait()는 예쁜 비동기 코드를 평범한 블로킹 호출로 바꿔버립니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION