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#에서는 이게 바로 async와 await입니다.
이것들은 허용합니다:
- 스레드(특히 UI나 서버 스레드)를 블로킹하지 않기.
- 쓸데없는 리소스 점유를 피하기.
- GUI 응답성 개선.
- 느린 작업이 진행되는 동안 스레드를 “잡아두지 않기”.
주의: 우리는 곧바로 모든 것을 비동기로 바꾸라고 강요하진 않습니다 — 비동기는 스레드와 블로킹에 대한 이해를 필요로 합니다. 블로킹 호출의 고통을 이해한 후라면 비동기 접근 방식이 정신적으로나 사용자 측면에서 얼마나 쾌적한지 느낄 수 있을 겁니다.
표: 무엇이 블로킹하고 무엇이 아닌가?
| 메서드/호출 | 스레드 블로킹 여부 | UI/Server에 적합한가 |
|---|---|---|
|
예 | 아니오 |
|
예 | 아니오 |
|
예 | 아니오 |
|
예 | 아니오 |
|
예 | 아니오 |
|
아니요 | 예 |
|
아니요 | 예 |
|
아니요 | 예 |
참고: 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()는 예쁜 비동기 코드를 평범한 블로킹 호출로 바꿔버립니다.
GO TO FULL VERSION