티스토리 뷰

728x90
반응형

[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
댓글