← Study Hub

07 · GCD → Swift Concurrency

스레드 폭발의 진짜 원인과 completion handler의 구조적 결함
진행률
0 / 0 완료

← 문항 인덱스

면접 질문
  1. GCD의 스레드 폭발(thread explosion) 문제와 Swift Concurrency의 해결 방식은?
  2. completion handler 방식의 구조적 문제 3가지는?

답변 ① — 범인은 개수가 아니라 블로킹이다 🔥 핵심

스레드 폭발의 원인은 "작업이 많아서"가 아니라 블로킹입니다. GCD의 작업 항목이 스레드를 붙잡고 기다리면(DispatchSemaphore.wait, 동기 I/O, 락, DispatchQueue.sync), GCD는 대기 중인 다른 항목을 처리하려고 새 스레드를 계속 만듭니다. 블로킹된 항목이 충분히 많으면 스레드가 코어 수를 훨씬 넘겨 수백 개까지 늘어나 컨텍스트 스위칭 폭증·메모리 낭비·우선순위 역전이 생깁니다. 반대로 짧고 블로킹하지 않는 작업은 아무리 많아도 폭발하지 않습니다.

Swift Concurrency는 협력적 스레드 풀(cooperative thread pool)로 해결합니다. 동시에 도는 스레드를 대략 코어 수로 제한하고, await에서는 스레드를 블로킹하지 않고 반납(suspend)해 그 스레드로 다른 작업을 돌립니다. 즉 task ≠ thread라서 수천 개의 task도 소수의 스레드로 처리합니다.

⚠️ 주의

뒤집어 말하면 이 풀에는 탈출 밸브가 없습니다. cooperative 스레드에서 블로킹하면(sleep()·세마포어·동기 락) 폭발이 아니라 기아(starvation)와 데드락이 납니다. 그래서 여기서는 블로킹이 금지이고 await·Task.sleep을 씁니다. "GCD 문제를 해결했다"까지만 답하고 이 반대급부를 모르면 반쪽짜리입니다.

GCD — 블로킹하면 스레드를 계속 늘린다 blocked blocked blocked blocked blocked blocked blocked blocked 스레드가 코어 수를 넘겨 계속 증가 → 컨텍스트 스위칭·메모리 폭증 Swift Concurrency — 스레드는 ≈코어 수, task가 양보한다 working working working working ← 스레드는 여기까지(≈코어 수) task 1000개가 대기·양보하며 이 스레드들을 돌려 쓴다 await = 스레드 반납(블로킹 아님) → task ≠ thread
GCD는 블로킹된 항목마다 스레드를 새로 만들어 폭발한다. 협력적 풀은 스레드 수를 고정하고, await에서 스레드를 반납해 재사용한다.
❌ 스레드 폭발을 부르는 패턴 vs ✅ 양보하는 패턴 (Swift)
// ❌ 스레드를 '붙잡고' 기다린다 → GCD가 새 스레드를 계속 만든다
for _ in 0..<100 {
    DispatchQueue.global().async {
        let sema = DispatchSemaphore(value: 0)
        fetchAsync { sema.signal() }
        sema.wait()          // 이 스레드는 여기서 묶인다(블로킹)
    }
}

// ✅ await는 스레드를 '반납'한다(블로킹 아님) → task ≠ thread
await withTaskGroup(of: Data.self) { group in
    for url in urls { group.addTask { await fetch(url) } }   // 수천 개도 소수 스레드로
}

답변 ② — 강제되지 않고, 뒤섞이고, 어디서 오는지 모른다

completion handler의 구조적 문제는 세 가지입니다.

completion handler의 3가지 구조적 결함 (Swift)
func load(completion: @escaping (Data?, Error?) -> Void) { /* ... */ }
// 1) completion을 깜빡하거나 두 번 호출해도 컴파일러가 못 막는다
// 2) (Data?, Error?) — 둘 다 nil인 '불가능한 상태'까지 타입이 허용 → 언래핑 지옥
// 3) 콜백이 어느 큐/스레드에서 오는지 불명확 → 레이스 · 메인스레드 크래시

// ✅ async: 반환 강제 · 값은 non-optional · 에러는 throw로 갈린다
func load() async throws -> Data { /* ... */ }

쉽게 이해하기

(질문 ①) GCD의 스레드 폭발은 이런 거예요. 일꾼(스레드)한테 일을 시켰는데, 그 일이 "전화 올 때까지 가만히 서서 기다려"래요. 일꾼이 멍하니 서 있으니 일이 밀리고, 사장은 밀린 일을 하려고 새 일꾼을 자꾸 뽑습니다. 그렇게 계속 뽑다 보면 사무실이 노는 사람으로 꽉 차버려요. 월급(메모리)은 나가는데 일은 안 되죠. 여기서 중요한 건 일이 많아서가 아니라 일꾼이 멍하니 서 있어서 생긴 문제라는 점이에요.

Swift는 규칙을 바꿨어요. "기다릴 거면 자리 비켜주고 다른 일 해!" 그래서 일꾼 수를 딱 필요한 만큼(대략 코어 수)만 두고, 일감이 천 개여도 몇 명이 번갈아 처리합니다. 대신 여기서는 멍하니 서 있기가 금지예요. 일꾼이 몇 명 없는데 그중 하나가 서 있으면, 이번엔 사람을 더 뽑는 게 아니라 일이 아예 멈춰버리거든요.

(질문 ②) completion handler는 "다 되면 연락 줘"라고 남긴 쪽지예요. 문제가 셋입니다. 상대가 연락을 깜빡해도 아무도 안 잡아주고(나는 영원히 기다림), 연락이 와도 "됐다는 건지 안 됐다는 건지 애매하게 오고(둘 다 비어 있을 수도 있음), 어느 문으로 올지 모릅니다(어느 스레드에서 올지 보장 없음).

🔑 한 문장

폭발의 범인은 작업의 개수가 아니라 블로킹이다. await는 기다리는 대신 자리를 내주는 것이다.