Task Closure Lifetime & Isolation
Task 클로저 라이프타임, isolated/non-isolated 개념, weak self 사용 시점 정리
Q1. "struct, class, Task, TaskGroup, 클로저는 non-isolated" 무슨 뜻?
isolated vs non-isolated
| 구분 | isolated (격리됨) | non-isolated (격리 안 됨) |
|---|---|---|
| 정의 | serial executor를 가진 것 | serial executor가 없는 것 |
| 예시 | Actor, @MainActor | struct, class, Task, TaskGroup, 클로저 |
| 동시 접근 | 한 번에 하나의 Task만 | 여러 Task가 동시에 가능 |
| Data race | 자동 보호 ✅ | 위험 (class의 경우) ❌ |
struct는 non-isolated인데 왜 안전해?
struct Point { // ← non-isolated (serial executor 없음)
var x: Double
}
Task {
var p1 = Point(x: 10) // ← 복사본 1
p1.x = 20
}
Task {
var p2 = Point(x: 10) // ← 복사본 2 (별개)
p2.x = 30
}
// → 각자 다른 복사본이므로 data race 없음
// → non-isolated지만 값 타입이라 안전
• isolated = serial executor 있음 (Actor)
• non-isolated = serial executor 없음 (struct, class, Task 등)
• struct는 non-isolated이지만 값 복사 덕분에 안전
Q2. await Task { }로 순차 실행하면 class도 안전해?
await Task { }.value의 동작
class Counter {
var count = 0
func increment() { count += 1 }
}
let counter = Counter()
// ✅ 순차 실행 - class여도 안전
await Task { counter.increment() }.value // Task 1 완료까지 기다림
await Task { counter.increment() }.value // Task 1 끝난 후 Task 2
await Task { counter.increment() }.value // Task 2 끝난 후 Task 3
// → count = 3 (항상) ✅
await 없이 동시 생성하면?
// ❌ 위험 - 동시 실행
Task { counter.increment() } // 즉시 시작
Task { counter.increment() } // 즉시 시작 (동시!)
Task { counter.increment() } // 즉시 시작 (동시!)
// → 3개 Task가 동시에 실행됨
// → data race ❌
// → count = ??? (1, 2, 또는 3)
class Counter를 nonisolated Task 클로저가 캡처하는 것 자체가 컴파일 에러(Sendable 위반)로 차단된다.
즉 최신 모드에선 "런타임 race"가 아니라 빌드 단계에서 거부된다. (그래서 actor·Sendable·await 순차화가 필요)
Actor는 await 없이도 안전
actor Counter {
var count = 0
func increment() { count += 1 }
}
let counter = Counter()
// ✅ 안전 - Actor가 순차 처리
Task { await counter.increment() } // 동시 생성
Task { await counter.increment() } // 동시 생성
Task { await counter.increment() } // 동시 생성
// → Task들은 동시에 생성되지만
// → Actor의 serial executor가 increment() 순차 처리
// → count = 3 (항상) ✅
await 없이 Task를 동시에 여러 개 만들어도 자동으로 순차 처리
→ 병렬성 유지 + 안전 보장
Q3. Task 1, 2, 3 실행 순서가 보장돼?
Task { await counter.increment() } // Task 1 생성
Task { await counter.increment() } // Task 2 생성
Task { await counter.increment() } // Task 3 생성
보장되는 것 vs 안 되는 것
| 보장됨 ✅ | 보장 안 됨 ❌ |
|---|---|
| Task 1, 2, 3 코드 순서대로 생성 | increment() 실행 순서 |
| Actor가 한 번에 하나씩 실행 | 어떤 Task가 먼저 실행될지 |
| 최종 count = 3 | Task 2가 Task 1보다 먼저 실행 가능 |
순서까지 보장하려면?
// 방법 1: await으로 완료 대기
await Task { await counter.increment() }.value
await Task { await counter.increment() }.value
await Task { await counter.increment() }.value
// 방법 2: 그냥 순차 호출
await counter.increment()
await counter.increment()
await counter.increment()
Q4. Task 클로저 라이프타임 - GCD도 동일해?
Task 클로저 라이프타임
actor Worker {
var work: Task<Void, Never>?
var result: Work?
deinit {
print("deinit actor")
}
func start() {
work = Task {
print("start task work")
try? await Task.sleep(for: .seconds(3))
self.result = Work() // self 캡처
print("completed task work")
// 완료되면 클로저 해제 → self 참조 해제
}
}
}
await Worker().start()
// 출력:
// start task work
// completed task work
// deinit actor ← 정상 해제!
GCD도 동일
class Worker {
var workItem: DispatchWorkItem?
var result: Work?
deinit {
print("deinit class")
}
func start() {
workItem = DispatchWorkItem {
print("start work")
Thread.sleep(forTimeInterval: 3)
self.result = Work() // self 캡처
print("completed work")
// 완료되면 클로저 해제 → self 참조 해제
}
DispatchQueue.global().async(execute: workItem!)
}
}
Worker().start()
// 출력:
// start work
// completed work
// deinit class ← 정상 해제!
1. 클로저가 self를 강하게 캡처
2. self가 작업 객체를 강하게 참조 (reference cycle처럼 보임)
3. 작업 완료 → 클로저 해제 → self 참조 해제
4. reference cycle 깨짐 → deinit 호출 ✅
Q5. weak self는 언제 써야 해?
weak self가 필요한 경우
class MyViewController: UIViewController {
func fetchData() {
Task { [weak self] in
let data = await api.fetch() // 5초 걸림
self?.updateUI(data) // ← VC 없으면 실행 안 됨
}
}
}
시나리오
// weak self 사용 시:
1. VC 진입
2. fetchData() 호출 (5초 걸리는 작업)
3. 사용자가 2초 후 뒤로가기
4. VC deinit (self = nil) ← 즉시 해제 ✅
5. 3초 후 작업 완료
6. self?.updateUI() → self가 nil이므로 실행 안 됨 ✅
// strong self (weak 안 씀) 시:
1. VC 진입
2. fetchData() 호출
3. 사용자가 뒤로가기
4. VC deinit 안 됨! (Task가 self 붙잡고 있음) ❌
5. 작업 완료
6. self.updateUI() 실행 (화면에 없는데!)
7. 그제서야 VC deinit
비교 표
| 상황 | weak self | strong self |
|---|---|---|
| 뒤로가기 시 VC 해제 | 즉시 해제 ✅ | 작업 완료 후 해제 ❌ |
| updateUI 호출 | 안 됨 ✅ | 됨 (불필요한 작업) ❌ |
| 메모리 | 효율적 ✅ | 낭비 ❌ |
weak self가 불필요한 경우
// 작업이 빨리 끝나고, 완료 후 반드시 실행해야 할 때
Task {
let result = await quickOperation() // 빠름
self.saveResult(result) // 반드시 저장해야 함
}
// → weak self 불필요
// → 어차피 작업 완료 후 self 해제됨
📌 핵심 정리
• isolated = serial executor 있음 (Actor)
• non-isolated = serial executor 없음 (struct, class, Task)
• struct는 non-isolated이지만 값 복사로 안전
• await으로 완료 기다리면 순차 실행
• class여도 안전
• Actor는 await 없이도 순차 처리
• Task 생성 순서 ≠ 실행 순서
• 순서 보장 안 됨
• 순서 필요하면 await으로 완료 대기
• Task, GCD 모두 완료 후 클로저 해제
• self 참조도 함께 해제
• reference cycle 자동 해소
• 작업 완료 전 객체 해제 원할 때
• VC 뒤로가기 시 즉시 해제 원할 때
• 불필요한 작업 방지
• 작업이 빠르게 완료될 때
• 완료 후 반드시 실행해야 할 때
• 어차피 완료 후 self 해제됨