안녕하세요, 루피입니다!
오늘은 Dispatch Queue의 종류에 대해 정리해 보겠습니다. 바로 시작합니다.
시작하기 전에..
많은 분들이 큐의 종류를 헷갈려 하십니다. 이렇게 정리하면 쉽습니다! Dispatch Queue라는 큰 우산 아래, 작업 처리 방식에 따라 Serial Queue와 Concurrent Queue로 나뉩니다.
- Serial Queue: 한 번에 하나의 작업만 순서대로 처리합니다.
- Concurrent Queue: 여러 작업을 동시에 처리할 수 있습니다.
그리고 이 기본적인 두 특성을 바탕으로 시스템이 제공하는 큐와 우리가 직접 만드는 큐가 있습니다.
- 시스템 제공 큐:
- Main Queue: 시스템이 기본으로 제공하는 Serial Queue.
- Global Queue: 시스템이 기본으로 제공하는 Concurrent Queue.
- Private Queue (Custom Queue): 개발자가 직접 생성하는 큐.
Serial또는Concurrent특성을 자유롭게 지정할 수 있으며, 지정하지 않으면 기본적으로Serial로 동작합니다.
Main Queue
Main Queue는 앱의 메인 스레드에서 작업을 처리하는 특별한 Serial Queue입니다. 주로 UI 업데이트처럼 반드시 메인 스레드에서 처리해야 하는 작업을 담당합니다.
// Main Queue 가져오기
let mainQueue = DispatchQueue.main
Main Queue는 왜 마음대로 멈출 수 없을까?
mainQueue에 suspend()나 resume()을 호출해도 아무런 효과가 없습니다. Main Queue는 시스템이 앱의 생명주기와 UI 반응성을 위해 직접 관리하기 때문입니다.
만약 개발자가 Main Queue를 마음대로 멈출 수 있다면, 버튼 클릭이나 화면 전환이 먹통이 되는 등 심각한 UI 프리징이 발생할 수 있습니다. 이는 사용자 경험을 크게 해치며, iOS의 감시 시스템(Watchdog)이 앱을 강제로 종료시키는 0x8badf00d 예외를 발생시킬 수 있습니다.
[Watchdog과 0x8badf00d 예외]
iOS는 앱의 메인 스레드가 너무 오랫동안 멈춰 있으면, 시스템 전체의 안정성을 위해 앱을 강제 종료합니다. 이때 나타나는 코드가 바로 0x8badf00d입니다. 네트워크 요청처럼 오래 걸리는 작업을 Main Queue에서 처리하면 이 에러를 만날 확률이 높습니다.
우리는 어떻게 Main Queue를 사용하고 있었을까?
우리는 코드를 직접 작성하지 않아도 이미 Main Queue를 사용하고 있습니다. SwiftUI나 UIKit 프로젝트를 생성할 때 볼 수 있는 @main 어노테이션 덕분입니다.
@main
struct MyApp: App {
// ...
}
이 @main이 내부적으로 UIApplicationMain을 호출하며, 이 과정에서 앱의 메인 스레드와 Main Queue가 자동으로 설정되고 실행됩니다.
🚨 MainQueue.sync { }: 데드락(Deadlock)
MainQueue.sync를 메인 스레드에서 호출하면 100% 데드락이 발생하여 앱이 멈춥니다.
// 이 코드는 메인 스레드(예: 버튼 액션)에서 실행 시 반드시 앱을 멈추게 합니다.
print("A", Thread.current) // 1. 메인 큐에서 작업 'ㄱ' 시작
DispatchQueue.main.sync { // 3. 메인 큐에 작업 'ㄴ'을 '동기(sync)'로 보냄
print("B", Thread.current)
}
print("K", Thread.current)
왜 데드락이 발생할까요?
- 버튼 클릭 액션(작업 'ㄱ')이 Main Queue에서 실행됩니다.
- 코드 중간에
main.sync로 새로운 작업('ㄴ')을 Main Queue에 보냅니다. sync(동기) 특성 때문에, 작업 'ㄱ'은 작업 'ㄴ'이 끝날 때까지 기다려야 합니다.- 하지만 Main Queue는
Serial(직렬) 큐이므로, 현재 실행 중인 작업 'ㄱ'이 끝나야만 다음 작업 'ㄴ'을 시작할 수 있습니다.
결과적으로 'ㄱ'은 'ㄴ'을 기다리고, 'ㄴ'은 'ㄱ'을 기다리는 영원한 대기 상태, 즉 데드락에 빠지게 됩니다.
그렇다면 SerialQueue.sync는 왜 괜찮을까?
결론부터 말하면, 작업을 보내는 큐와 작업을 받는 큐가 다르기 때문입니다.
let mySerialQueue = DispatchQueue(label: "my.serial.queue")
// 메인 스레드(예: 버튼 액션)에서 실행
print("A", Thread.current) // 1. Main Queue에서 작업 중
mySerialQueue.sync { // 2. 다른 큐(mySerialQueue)로 작업을 보냄
print("B", Thread.current)
}
print("K", Thread.current)
이 경우 Main Queue는 MySerialQueue에게 "이 작업('ㄴ') 좀 처리해줘, 끝날 때까지 기다릴게"라고 요청합니다. 두 큐는 별개이므로, MySerialQueue는 바로 작업 'ㄴ'을 처리할 수 있습니다. 'ㄴ'이 끝나면 Main Queue는 기다림을 멈추고 다음 코드를 실행합니다. 데드락이 발생하지 않습니다.
Global Queue
Global Queue는 시스템이 제공하는 기본 Concurrent Queue입니다. 주로 시간이 오래 걸리는 작업을 백그라운드 스레드에서 처리할 때 사용합니다.
// Global Queue 가져오기 (QoS 지정)
let globalQueue = DispatchQueue.global(qos: .userInitiated)
네트워크 요청, 데이터 처리, 파일 저장 등 UI를 방해하지 말아야 할 무거운 작업들을 처리하기에 적합합니다.
QoS: 작업의 우선순위 정하기
Global Queue는 QoS(Quality of Service) 클래스를 지정하여 작업의 중요도를 시스템에 알릴 수 있습니다.
userInteractive: UI와 직접 연관된, 즉각적인 반응이 필요한 작업. (가장 높음)userInitiated: 사용자가 시작한 작업으로, 빠른 결과가 필요할 때.default: 기본값.utility: 시간이 오래 걸리지만 즉각적인 결과가 필요 없는 작업.background: 사용자에게 보이지 않는 백그라운드 작업. (가장 낮음)unspecified: QoS 정보가 없음을 의미.
Global Queue 또한 시스템이 전적으로 관리하므로 suspend(), resume() 등의 메서드는 효과가 없습니다.
오늘도 화이팅입니다.
'iOS > Swift' 카테고리의 다른 글
| [Swift] Dispatch Work Item (0) | 2025.02.24 |
|---|---|
| [Swift] Dispatch Group (0) | 2025.02.23 |
| [Swift] Concurrent + Sync 에 대한 궁금증 (0) | 2025.02.19 |
| [Swift] DispatchQueue (0) | 2025.02.19 |
| [Swift] GCD를 공부해봅시다. (0) | 2025.02.18 |