안녕하세요! 오늘도 Concurrency에 대한 이야기를 이어가 보겠습니다.
우리가 Swift Concurrency를 사용하면서 애플과 새로운 계약을 맺은 것, 알고 계셨나요?
WWDC에서는 이 관계를 런타임 계약(Runtime Contract)이라는 멋진 단어로 표현했습니다.
천주교 신자인 제게는 마치 구약 시대를 지나 새로운 약속을 맺는 것과 같은 기분마저 들더군요.
그 계약의 핵심 조항은 단 하나입니다.
"어떤 경우에도 스레드를 블로킹(Blocking)하지 말 것."
이 간단한 원칙 하나가 Swift Concurrency의 모든 것을 설명합니다.
이 계약을 지키려면, 예전처럼 스레드를 마구 생성하며 동시성을 처리할 수 없습니다.
주어진 소수의 스레드만으로 모든 업무를 처리해야 하죠.
그래서 협동적 스레드 풀이 필요합니다.
왜 협동적이라는 이름이 붙었을까요? 이전 GCD의 방식과 비교해 보면 명확해집니다.
일이 밀리자 사장님(OS)은 계속 새 사람을 뽑아 해결하려 하죠. (스레드 폭발)
이처럼 await 키워드를 통해 "나 지금은 일 못해!"라고 시스템에 자발적으로 신호를 보내 CPU 제어권을 양보하기 때문에 협동적이라는 이름이 붙은 것입니다.
이전처럼 OS가 CPU 시간을 강제로 빼앗아오는 선점형 방식이 아닌, Task들이 서로를 위해 양보하는 협동형 방식으로 문화가 바뀐 것이죠.
| 특징 | GCD 스레드 풀 (선점형) | 협동적 스레드 풀 |
|---|---|---|
| 운영 방식 | OS가 CPU 시간을 강제로 빼앗고 분배 | Task가 await를 통해 자발적으로 CPU 양보 |
| 작업의 행동 | 차단 (Block) - 아무에게도 알리지 않고 멈춤 | 일시 중단 (Suspend) - 양보 의사를 밝히고 멈춤 |
| 결과 | 스레드 폭발, 높은 컨텍스트 스위칭 비용 | 소수의 스레드로 높은 효율, 낮은 오버헤드 |
하지만 이 자발적 양보는 새로운 문제를 낳았습니다. 바로 원자성(Atomicity)이 깨진다는 점입니다.
await는 코드의 흐름이 잠시 끊기는 지점이며, 그 사이 무슨 일이 일어날지 아무도 모릅니다.
바로 이 지점에서 아래와 같은 끔찍한 데이터 경쟁(Data Race)이 발생할 수 있습니다.
await를 만나 거래 기록을 기다리며 잠시 멈춤.await를 만나 거래 기록을 기다리며 잠시 멈춤.결과적으로 은행은 1600원을 내주었고, 장부는 마이너스가 되었습니다.
이 문제를 해결하기 위해 등장한 것이 바로 데이터 금고지기, Actor입니다. Actor는 자신의 데이터에 한 번에 하나의 작업만 접근하도록 문을 걸어 잠가 데이터 경쟁을 원천적으로 막습니다.
그런데 Actor는 GCD의 직렬 큐와 결정적인 차이점이 있습니다. 바로 재진입성(Re-entrancy)입니다.
직렬 큐 (재진입 불가): 융통성 없는 요리사. 10분 걸리는 스테이크를 굽기 시작하면, VIP 손님이 물 한 잔을 달라는 간단한 요청도 스테이크가 다 구워질 때까지 처리해주지 않습니다. (선입선출, FIFO)
Actor (재진입 가능): 똑똑한 요리사. 스테이크를 오븐에 넣어두고(await), 그 사이에 들어온 VIP 손님의 물 한 잔 요청을 먼저 처리해줍니다. await로 생긴 빈틈에 다른 작업이 들어올 수 있는 것이죠.
이 재진입 덕분에 Actor는 작업이 들어온 순서(FIFO)에 얽매이지 않고, 우선순위가 높은 작업을 먼저 처리하여 앱의 응답성을 극적으로 향상시킵니다.
이 강력한 기능을 올바르게 사용하기 위해 우리가 지켜야 할 약속 세 가지를 정리해 보았습니다.
"의자 하나 옮기자고 이사 업체를 부르시겠어요?"
Task를 만드는 데는 생각보다 큰 '관리 비용'이 듭니다. UserDefaults에서 값을 읽는 것처럼 아주 간단한 작업에 Task를 사용하는 것은 오히려 성능을 해칠 수 있습니다. 동시성은 네트워크 통신처럼 정말 '기다림'이 필요한 곳에 사용합시다.
await 앞뒤는 다른 세상임을 기억할 것"위에서 본 은행 시나리오처럼, await는 코드의 원자성을 깨뜨리는 명시적인 중단점입니다. await를 지난 후에는 이전에 확인했던 값이나 상태가 그대로 유지될 것이라고 절대 가정하면 안 됩니다. 이런 민감한 코드는 Actor로 보호해야 합니다.
"최첨단 AI 교통 관제 시스템이 있는데, 운전자가 멋대로 수동 차단기를 내리면 어떻게 될까요?"
Semaphore 같은 오래된 동기화 도구는 Swift 런타임(AI 교통 관제 시스템) 모르게 스레드를 멈춰버리는 '수동 차단기'와 같습니다. 런타임은 길이 막힌 이유를 몰라 잘못된 판단을 내리고, 결국 도시 전체(앱)가 마비되는 데드락에 빠질 수 있습니다.
런타임 계약을 지키려면, Swift Concurrency가 제공하는 Actor, TaskGroup 같은 최신 도구를 사용해야 합니다.