GCD는 작업(클로저)을 큐(Queue)에 제출하면, 스레드 풀이 자동으로 효율적인 스레드 배정과 실행을 담당하는 런타임 시스템입니다.
큐 타입 | 특성 | 용도 |
---|---|---|
Main Queue | 직렬 큐, UI 전용 | UI 업데이트, 메인 스레드 작업 |
Serial Queue | 순차 실행 (FIFO) | 순서가 중요한 작업, 데이터 일관성 |
Concurrent Queue | 병렬 실행 가능 | 독립적인 작업들의 병렬 처리 |
// 비동기 실행 (권장)
DispatchQueue.global().async {
// 백그라운드 작업
print("백그라운드 실행")
}
// 동기 실행 (주의 필요)
DispatchQueue.global().sync {
// 현재 스레드 블로킹
print("동기 실행 완료까지 대기")
}
"작업을 큐에 던지면, 시스템이 스레드를 적절히 배정해 실행해준다."
// ❌ 데드락 발생!
DispatchQueue.main.async {
// 메인 스레드에서 실행 중...
DispatchQueue.main.sync {
// 메인 큐에 sync로 작업 제출
print("이 코드는 실행되지 않음!")
}
}
main.sync
호출// ✅ 올바른 방법
DispatchQueue.main.async {
// UI 업데이트나 메인 스레드 작업
updateUI()
}
// ✅ 백그라운드에서 메인으로 전환
DispatchQueue.global().async {
// 백그라운드 작업
let result = heavyComputation()
DispatchQueue.main.async {
// UI 업데이트
self.updateUI(with: result)
}
}
메인 스레드에서 메인 큐로는 반드시 async
만 사용
구분 | 동시성 (Concurrency) | 병렬성 (Parallelism) |
---|---|---|
정의 | 여러 작업을 논리적으로 동시에 처리하는 구조 | 물리적으로 실제 동시 실행 |
구현 | 스케줄링, 인터리빙을 통한 겉보기 동시성 | 멀티코어를 활용한 진짜 동시 실행 |
의존성 | 단일 코어에서도 가능 | 멀티코어 하드웨어 필요 |
// 동시성: 구조적 동시 처리
DispatchQueue.global().async {
// 작업1 시작
}
DispatchQueue.global().async {
// 작업2 시작 (논리적 동시성)
}
// 병렬성: 실제 물리적 동시 실행 (시스템이 자동 결정)
// 사용 가능한 CPU 코어에 따라 실제 병렬 실행 여부 결정
방식 | 특성 | 장점 | 단점 |
---|---|---|---|
동기 | 결과가 올 때까지 대기 | 코드 흐름 단순, 에러 처리 직관적 | UI 블로킹, 자원 비효율 |
비동기 | 즉시 반환, 결과는 나중에 | 응답성 좋음, 자원 효율적 | 콜백 헬, 에러 처리 복잡 |
// 동기 방식
func fetchDataSync() -> Data {
let data = networkRequest() // 완료까지 대기
return data
}
// 비동기 방식 (콜백)
func fetchDataAsync(completion: @escaping (Data?) -> Void) {
networkRequest { data in
completion(data) // 나중에 결과 전달
}
}
// 비동기 방식 (async/await)
func fetchDataAsync() async -> Data {
let data = await networkRequest()
return data
}
func legacyAPIWrapper() async -> String {
return await withCheckedContinuation { continuation in
legacyAPI { result in
continuation.resume(returning: result)
}
}
}
func legacyAPIWrapperWithError() async throws -> Data {
return try await withCheckedThrowingContinuation { continuation in
legacyNetworkAPI { data, error in
if let error = error {
continuation.resume(throwing: error)
} else if let data = data {
continuation.resume(returning: data)
} else {
continuation.resume(throwing: APIError.unknown)
}
}
}
}
API | 사용 시점 | 특징 |
---|---|---|
WCC | 실패 개념이 없는 콜백 | 항상 성공, throw 없음 |
WCTC | 에러를 throw로 전파하고 싶은 경우 | 실패 시 예외 던짐 (가장 일반적) |
// 기존 콜백 기반 API
func requestLocationPermission(completion: @escaping (Bool) -> Void) {
// 권한 요청 로직
}
// async/await 래퍼
func requestLocationPermission() async -> Bool {
return await withCheckedContinuation { continuation in
requestLocationPermission { granted in
continuation.resume(returning: granted)
}
}
}
// 사용
let isGranted = await requestLocationPermission()
Actor는 데이터 경쟁(Data Race)을 방지하기 위해 해당 인스턴스의 상태 접근을 직렬화하는 동시성 격리 타입입니다.
actor BankAccount {
private var balance: Double = 0
func deposit(amount: Double) {
balance += amount // 안전한 상태 변경
}
func withdraw(amount: Double) -> Bool {
guard balance >= amount else { return false }
balance -= amount
return true
}
func getBalance() -> Double {
return balance
}
}
특징 | 설명 | 의미 |
---|---|---|
격리된 상태 | mutable state는 actor 내부에서만 안전 | 동시 접근 차단 |
비동기 접근 | 외부에서 접근 시 await 필요 | 컨텍스트 전환(hop) |
순차 실행 | 한 번에 하나의 작업만 실행 | 데이터 레이스 방지 |
let account = BankAccount()
// 외부에서 접근 시 await 필요
Task {
await account.deposit(amount: 100)
let balance = await account.getBalance()
print("현재 잔액: \(balance)")
}
actor Counter {
private var value = 0
func increment() async {
let current = value
await someAsyncWork() // 일시 중단 지점
value = current + 1 // ⚠️ 다른 태스크가 value를 변경했을 수도 있음
}
}
// ❌ 교착 위험
actor A {
func callB(_ b: B) async {
await b.callA(self) // A → B → A 순환 대기
}
}
actor B {
func callA(_ a: A) async {
await a.callB(self)
}
}
MainActor는 메인 스레드 전용 실행자에 격리된 글로벌 액터로, UI 업데이트와 관련된 코드가 항상 메인 스레드에서만 실행되도록 보장합니다.
@MainActor
class ViewModel: ObservableObject {
@Published var data: String = ""
// 메인 액터에 격리된 메서드
func updateUI(with newData: String) {
data = newData // 메인 스레드에서 안전하게 실행
}
// 백그라운드 작업
func fetchData() async {
// 백그라운드에서 실행
let result = await heavyComputation()
// 메인 액터로 자동 전환
updateUI(with: result)
}
}
@MainActor
class UIController {
var title: String = ""
func updateTitle() {
// 모든 멤버가 메인 액터에 격리됨
title = "새로운 제목"
}
}
class DataManager {
@MainActor
func updateUI() {
// 이 메서드만 메인 액터에서 실행
}
func processData() {
// 백그라운드에서 실행 가능
}
}
// 백그라운드에서 메인으로 전환
Task {
let data = await fetchFromAPI()
await MainActor.run {
// UI 업데이트를 배치로 처리
updateLabel(with: data.title)
updateImage(with: data.image)
}
}
특성 | Task | Task.detached |
---|---|---|
컨텍스트 상속 | 부모로부터 상속 | 독립 실행 |
취소 전파 | 부모 취소 시 자식도 취소 | 독립적 생명주기 |
액터 격리 | 부모 액터 상속 | 격리 없음 |
우선순위 | 부모 우선순위 상속 | 별도 지정 |
TaskLocal | 상속 | 독립 |
@MainActor
func viewDidLoad() {
Task { // MainActor 컨텍스트 상속
let data = await fetchData()
updateUI(with: data) // await 없이 호출 가능
}
}
@MainActor
func viewDidLoad() {
Task.detached { // 독립 실행
let data = await fetchData()
await MainActor.run { // 명시적 전환 필요
updateUI(with: data)
}
}
}
상황 | 권장 선택 | 이유 |
---|---|---|
일반적인 비동기 작업 | Task | 컨텍스트 상속으로 안전하고 편리 |
백그라운드 파이프라인 | Task.detached | UI와 완전 분리된 독립 작업 |
로깅, 분석 | Task.detached | 메인 작업에 영향 없는 독립 처리 |
순수 계산 작업 | Task.detached | 액터 격리가 불필요한 경우 |
Sendable은 스레드/액터 경계를 넘어가도 데이터 레이스 없이 안전하게 전달될 수 있음을 컴파일 타임에 보증하는 마커 프로토콜입니다.
// 자동으로 Sendable
struct UserProfile: Sendable {
let name: String
let age: Int
}
// 저장 프로퍼티가 Sendable이면 자동 합성
struct Response: Sendable {
let data: Data
let timestamp: Date
}
// ❌ 기본적으로 Sendable 아님
class MutableCounter {
var count = 0
}
// ✅ 불변 클래스는 Sendable 가능
final class ImmutableData: @unchecked Sendable {
let value: String
let timestamp: Date
init(value: String) {
self.value = value
self.timestamp = Date()
}
}
// Sendable 클로저 - 캡처된 값도 Sendable이어야 함
func processAsync(_ handler: @Sendable @escaping () -> Void) {
Task {
await someWork()
handler()
}
}
// 사용
let message = "완료" // String은 Sendable
processAsync {
print(message) // ✅ 안전한 캡처
}
class Counter {
var value = 0
}
let counter = Counter()
// ❌ 컴파일 에러: Counter는 Sendable이 아님
Task {
counter.value += 1 // 데이터 레이스 위험
}
// 1. Actor로 감싸기
actor SafeCounter {
private var value = 0
func increment() {
value += 1
}
func getValue() -> Int {
return value
}
}
// 2. 값 복사로 전달
let currentValue = counter.value
Task {
let newValue = currentValue + 1
// 안전한 처리
}
CPU가 실행 중인 스레드/태스크를 바꾸며 레지스터/스택 저장·복원하는 과정입니다.
await
지점에서 태스크 일시 중단// ❌ 빈번한 hop
@MainActor
func updateUI() async {
title = await computeTitle() // hop 1
subtitle = await computeSubtitle() // hop 2
image = await loadImage() // hop 3
}
// ✅ 배치 처리
func updateUI() async {
let (title, subtitle, image) = await (
computeTitle(),
computeSubtitle(),
loadImage()
)
await MainActor.run {
self.title = title
self.subtitle = subtitle
self.image = image
}
}
// ❌ 과도한 분할
for item in items {
Task {
await process(item) // 수천 개의 작은 태스크
}
}
// ✅ 배치 처리
let batches = items.chunked(into: 100)
for batch in batches {
Task {
await processBatch(batch)
}
}
@MainActor
class ViewModel {
var data: [Item] = []
// ✅ 계산은 격리 해제
nonisolated func processData(_ input: [RawData]) -> [Item] {
return input.map { Item(from: $0) }
}
func loadData() async {
let rawData = await fetchFromAPI()
let processed = processData(rawData) // hop 없음
data = processed // 메인 액터에서 상태 업데이트
}
}
업무량 대비 과도한 스레드가 생성되어 오버서브스크립션 상태가 되는 현상으로, 성능 저하나 시스템 불안정을 야기합니다.
// ❌ 스레드 폭발 위험
for i in 0..<1000 {
DispatchQueue.global().async {
Thread.sleep(forTimeInterval: 1.0) // 블로킹
print("작업 \(i) 완료")
}
}
// ❌ 제한 없는 Task 생성
for url in urls { // urls.count = 10000
Task {
await downloadImage(from: url)
}
}
// ❌ 동기적 네트워크 요청
let data = try Data(contentsOf: url) // 블로킹
// ✅ 비동기 네트워크 요청
let (data, _) = try await URLSession.shared.data(from: url)
// ✅ 동시 실행 수 제한
func downloadImages(urls: [URL]) async {
await withTaskGroup(of: Void.self) { group in
let semaphore = AsyncSemaphore(value: 5) // 최대 5개 동시 실행
for url in urls {
group.addTask {
await semaphore.wait()
defer { semaphore.signal() }
await downloadImage(from: url)
}
}
}
}
// ✅ 작업을 배치로 묶어서 처리
func processLargeDataset(_ items: [DataItem]) async {
let batchSize = 100
let batches = items.chunked(into: batchSize)
for batch in batches {
await processBatch(batch)
}
}
// ✅ 적절한 우선순위 설정
Task(priority: .background) { // UI에 영향 없는 백그라운드 작업
await processAnalytics()
}
Task(priority: .userInitiated) { // 사용자 요청 작업
await loadUserProfile()
}
// ✅ 역할별로 분리된 액터
actor NetworkCache {
private var cache: [String: Data] = [:]
func store(data: Data, for key: String) {
cache[key] = data
}
func retrieve(for key: String) -> Data? {
return cache[key]
}
}
actor UserSessionManager {
private var currentUser: User?
private var sessionToken: String?
func login(user: User, token: String) {
currentUser = user
sessionToken = token
}
func logout() {
currentUser = nil
sessionToken = nil
}
}
actor FileProcessor {
private var state: ProcessingState = .idle
private var currentFile: File?
func startProcessing(_ file: File) async throws {
guard state == .idle else {
throw ProcessorError.alreadyProcessing
}
state = .processing
currentFile = file
do {
await processFile(file)
state = .completed
} catch {
state = .failed
throw error
}
}
private func processFile(_ file: File) async {
// await 지점에서도 상태가 일관되도록 설계
}
}
// ✅ 데이터를 복사해서 전달
actor OrderProcessor {
func processOrder(_ order: Order) async {
let orderData = OrderData(from: order) // 복사본 생성
await paymentService.processPayment(orderData) // 안전한 전달
}
}
이러한 개념들은 iOS 개발에서 안전하고 효율적인 동시성 프로그래밍의 핵심입니다.