← 홈으로 돌아가기
Swift Concurrency Q&A

Task Closure Lifetime & Isolation

Task 클로저 라이프타임, isolated/non-isolated 개념, weak self 사용 시점 정리

Task Actor Isolation Memory
참고 문서

Apple Developer - Task

Q1. "struct, class, Task, TaskGroup, 클로저는 non-isolated" 무슨 뜻?

핵심: serial executor가 없어서 여러 Task가 동시에 접근 가능하다는 의미

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 완료를 기다리면 순차 실행이므로 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)
⚠️ 보강: 위 예시의 data race는 Swift 5 기본 모드 기준 "런타임에 발생하는" 문제다. Swift 6 / strict concurrency에서는 non-Sendable인 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 (항상) ✅
Actor의 장점:
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도 동일해?

답: 네, 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는 언제 써야 해?

핵심: 작업 완료 전에 객체가 해제되길 원할 때 (예: VC 뒤로가기)

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 vs non-isolated

• isolated = serial executor 있음 (Actor)
• non-isolated = serial executor 없음 (struct, class, Task)
• struct는 non-isolated이지만 값 복사로 안전

await Task { } 순차 실행

• await으로 완료 기다리면 순차 실행
• class여도 안전
• Actor는 await 없이도 순차 처리

Task 실행 순서

• Task 생성 순서 ≠ 실행 순서
• 순서 보장 안 됨
• 순서 필요하면 await으로 완료 대기

클로저 라이프타임

• Task, GCD 모두 완료 후 클로저 해제
• self 참조도 함께 해제
• reference cycle 자동 해소

weak self 사용 시점

• 작업 완료 전 객체 해제 원할 때
• VC 뒤로가기 시 즉시 해제 원할 때
• 불필요한 작업 방지

weak self 불필요한 경우

• 작업이 빠르게 완료될 때
• 완료 후 반드시 실행해야 할 때
• 어차피 완료 후 self 해제됨

📚 참고 자료