- GCD의 스레드 폭발(thread explosion) 문제와 Swift Concurrency의 해결 방식은?
- 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 문제를 해결했다"까지만 답하고 이 반대급부를 모르면 반쪽짜리입니다.
await에서 스레드를 반납해 재사용한다.// ❌ 스레드를 '붙잡고' 기다린다 → 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의 구조적 문제는 세 가지입니다.
- 컴파일러가 호출을 강제하지 못함 — 어떤 분기에서 콜백을 안 부르면 호출자는 영원히 결과를 못 받고, 두 번 부르면 후속 로직이 두 번 실행됩니다. 둘 다 컴파일 타임에 안 잡힙니다. 반면 async 함수는 컴파일러가 반환을 강제합니다.
- 성공/에러가 옵셔널로 뒤섞임 —
(data?, response?, error?)처럼 "둘 다 nil"인 불가능한 상태까지 타입이 허용해서, 호출부마다 언래핑 보일러플레이트가 붙습니다. async는 값이 non-optional로 오거나throw로 갈립니다. - 콜백 지옥 + 전달 큐/스레드 불명확 — 중첩이 깊어져 가독성이 무너지고, 콜백이 어느 큐에서 오는지 API 계약에 없어 레이스나 "메인 스레드 아님" 크래시가 납니다. 덤으로 구조적 취소·에러 전파가 불가능합니다.
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는 기다리는 대신 자리를 내주는 것이다.