티스토리 뷰
[1] 목표
Swift 에서 deinit 은 항상 nonisolated 입니다.
객체 해제는 즉시 일어나야 하고, Executor 전환으로 지연되면 안되기 때문에
deinit 을 특정 Executor (ex. @MainActor) 에 격리 불가능합니다.
따라서 아래 세가지 상황에서 늘 헷갈리는데 정리해봅시다.

[2.1] 상황 1

현황)
이 상황은 swift 5 에서도 에러 발생합니다.
Strict Concurrency Checking 을 가장 낮은 수준 (Minimal) 로 해도 에러 발생.

원인)
• deinit 안의 코드는 nonisolated 상태로 실행됨.
→ 그런데 effect1.stop()은 @MainActor 격리된 메서드.
→ nonisolated(deinit)에서 actor-isolated(stop()) 호출 → 컴파일러가 막음.
• deinit은 “스레드 안 가리고 지금 당장 파괴해!” 모드.
• 그런데 stop()은 “꼭 메인 액터에서만 불러!”라는 규칙.
• 그래서 두 규칙이 충돌 → 에러.
해결)
// 1) 지역 변수로 뽑아내기
deinit {
let e = effect1
Task { @MainActor in
e.stop()
}
}
// 2) capture list로 직접 캡처
deinit {
Task { @MainActor [effect1] in
effect1.stop()
}
}
• deinit은 nonisolated → @MainActor 메서드(stop)를 직접 호출 불가.
• 그래서 메인 액터로 hop (전환) 하는 작업만 예약 (Task { @MainActor in ... }) 하고 즉시 deinit을 끝냄
• let e = effect1로 강하게 캡처해 Task가 실행될 때까지 effect1의 수명을 잠시 연장 (사용 후 자동 해제).
+ 두 방법 다 내부적으로 동일한 동작을 하고 스타일 차이일 뿐입니다.
질문1)
만약 self 를 직접 캡쳐한다면 어떻게 되는가 ?

위의 코드에서 컴파일러는 effect1 에 접근하려면 결국 self 를 캡쳐해야한다고 판단하고
swift 5 에서는 워닝, swift 6 에서는 에러를 냅니다. Capture of 'self' in a closure that outlives deinit
deinit 안에서 만든 비동기 클로저(Task)가 self보다 더 오래 살아있을 수 있는데, 그 클로저가 self(를 통해 effect1)를 캡처했다. 그래서 금지!
- deinit { Task { … } }에서 Task 클로저는 탈출(escaping) 하며, deinit 종료 후에도 실행될 수 있음
- effect1.stop()를 부르려면 effect1(= self.effect1)에 접근해야 하므로 self를 캡처함.
- 하지만 deinit 이후 self는 해제되기 때문에, “deinit보다 오래 사는 클로저가 self를 잡는 것”은 안전하지 않아 컴파일러가 막음.
하지만 위의 해결 방안 처럼 하면 클로저는 self 가 아니라 effet1 만 캡쳐해서 괜찮은 것입니다.
// 1) 지역 변수로 뽑아내기
deinit {
let e = effect1
Task { @MainActor in
e.stop()
}
}
// 2) capture list로 직접 캡처
deinit {
Task { @MainActor [effect1] in
effect1.stop()
}
}
- 두 경우 모두 self를 직접 캡처하지 않고,
- effect1 라는 프로퍼티의 값을 deinit 시점에 지역 변수처럼 캡처합니다.
- 따라서 클로저는 self가 아니라 effect1 만 들고 있게 되고,
- 👉 “Capture of ‘self’ in a closure that outlives deinit” 에러가 사라지는 거예요.
질문 2)
strong 이 아니라 weak 하게 캡쳐한다면 ?

• Task가 도는 시점에 effect가 이미 해제됐으면 아무 일도 안 함.
• → 정리 로직이 건너뛰어질 수 있음(누수는 아니더라도 의도한 정리 미실행 가능).
따라서
• 확실히 정리해야 함 → strong 캡처
• 있으면 하고, 없어도 됨 → weak 캡처
stop이 꼭 호출돼야 하는 중요 리소스 정리(타이머 invalidate, 관찰 해제, 파일 핸들 닫기 등) 에서는 strong 권장
[2.2] 상황 2

현황)
Swift 6 + Strict Concurrency Checking 을 가장 높은 수준 (Complete) 로 해도 에러 안남
설명)
1. 프로퍼티 접근이 가능한 이유 = Sendable
• nonisolated 컨텍스트에서 메인 액터 격리 프로퍼티에 접근하려면, 그 값이 실행자 경계를 안전하게 건널 수 있어야(Sendable) 하는데, SomeUIEffect2는 Sendable 이다. → deinit에서 effect2 접근 OK.
2. 메서드 호출이 가능한 이유 = @MainActor 아님
• stop()이 액터 격리(예: @MainActor)가 아니므로, hop(실행자 전환) 없이 동기 호출 가능 → OK.
요약:
• 프로퍼티 접근 측면: Sendable이라 안전.
• 메서드 호출 측면: @MainActor가 아니니 hop 불필요.
[2.3] 상황 3

현황)
swift5 에서는 워닝, swift 6 에서는 에러 발생
원인)
• SomeUI가 @MainActor라서 effect3 자체가 메인 액터에 격리돼 있어요.
• 그런데 deinit은 항상 nonisolated(아무 실행자에도 속하지 않음).
• nonisolated 컨텍스트에서 메인 액터 격리 프로퍼티에 접근하려면, 그 값이 실행자 경계를 안전하게 건널 수 있어야(Sendable) 하는데, SomeUIEffect3는 Sendable이 아니므로 프로퍼티를 읽는 순간 막힙니다.
→ "Cannot access property 'effect3' with a non-Sendable type ... from nonisolated deinit"
해결)
우선 1번과 같은 방식으로 해결할 수 없습니다.

deinit은 nonisolated이고, effect3는 @MainActor에 격리된 프로퍼티라서 그 접근 자체가 크로스-액터 접근이 되는데
non-sendable 이여서 크로스-액터 접근이 불가능하기 때문입니다.
풀어서 이해해보면 다음과 같습니다.
• deinit은 nonisolated라서 어떤 액터에도 속하지 않습니다.
• effect3는 @MainActor 격리된 저장 프로퍼티라 **액터 경계 밖(= nonisolated)**에서 접근하면 크로스-액터 접근이 됩니다.
• 이때 **값을 액터 경계 밖으로 “꺼내도 안전한지”**를 컴파일러가 따지는데, 그 안전성의 기준이 Sendable 입니다.
• SomeUIEffect3가 non-Sendable이므로 경계 밖으로 가져올 수 없어 접근 자체가 거부됩니다 → 에러.
따라서 요런 식으로 약간의 트릭을 써야합니다.
final class DeinitAction {
let action: () -> Void
init(_ action: @escaping () -> Void) {
self.action = action
}
deinit {
action()
}
}
@MainActor
public final class SomeUI {
let effect3 = SomeUIEffect3()
private let onDeinit: DeinitAction
init() {
onDeinit = DeinitAction { [effect3] in
effect3.stop()
// 만약 stop이 메인에서 실행되어야한다면
// Task { @MainActor in effect3.stop() }
}
effect3.start()
}
deinit { /* 비워둬도 됨. onDeinit이 해제되며 action 실행 */ }
}
[3] 정리

final class DeinitAction {
let action: () -> Void
init(_ action: @escaping () -> Void) {
self.action = action
}
deinit {
action()
}
}
@MainActor
public final class SomeUI {
let effect1 = SomeUIEffect1()
let effect2 = SomeUIEffect2()
let effect3 = SomeUIEffect3()
private let onDeinit: DeinitAction
init() {
onDeinit = DeinitAction { [effect3] in
effect3.stop()
// 만약 stop이 메인에서 실행되어야한다면
// Task { @MainActor in effect3.stop() }
}
effect1.start()
effect2.start()
effect3.start()
}
deinit {
Task { @MainActor [effect1] in
effect1.stop()
}
effect2.stop()
}
}
@MainActor
final class SomeUIEffect1 {
func start() {}
func stop() {}
}
final class SomeUIEffect2: Sendable {
func start() {}
func stop() {}
}
final class SomeUIEffect3 {
func start() {}
func stop() {}
}
'🍏 > Swift' 카테고리의 다른 글
| [Swift] Foundation Models (1) | 2025.06.13 |
|---|---|
| [Swift] Swift Testing 헷갈리는 것 정리 (0) | 2025.01.29 |
| [Swift] 매크로 (Macro) (4) | 2024.12.07 |
| [Swift] @isolated(any) (4) | 2024.11.15 |
| [Swift] @unchecked, @preconcurrency, @retroactive (9) | 2024.11.06 |
- Total
- Today
- Yesterday
- 장고 Custom Management Command
- Flutter Spacer
- flutter 앱 출시
- Dart Factory
- Django FCM
- Flutter 로딩
- flutter deep link
- cocoapod
- flutter dynamic link
- Python Type Hint
- 구글 Geocoding API
- 플러터 얼럿
- 장고 URL querystring
- Django Heroku Scheduler
- 플러터 싱글톤
- METAL
- DRF APIException
- Sketch 누끼
- github actions
- PencilKit
- ribs
- ipad multitasking
- Flutter Clipboard
- Flutter Text Gradient
- Watch App for iOS App vs Watch App
- Flutter getter setter
- SerializerMethodField
- drf custom error
- Django Firebase Cloud Messaging
- flutter build mode
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |