지연된 Swift 계산은 작업이 소유 객체를 강하게 캡처하면 해당 소유 객체를 계속 유지합니다. 기다리는 동안 소유 객체가 사라질 수 있도록 하려면 약한 캡처를 사용하고, 소유 객체 없이도 계산을 완료해야 한다면 독립적인 값 스냅샷을 캡처하세요.
간단한 데스크톱 계산 미리보기를 만드는 Mac 개발자에게 실무적으로 필요한 일은 객체의 수명을 의도적으로 선택하는 것입니다. 이 예제는 잠시 기다린 뒤 합성 값 [4, 8, 12]을 더하고 합계를 서식화합니다. 모든 작업은 로컬 메모리에서 실행됩니다.
계산의 수명은 어떻게 설정해야 할까요?
Swift 캡처 목록을 사용하면 클로저가 무엇을 유지할지 지정할 수 있습니다. 강한 캡처는 참조된 인스턴스를 유지하고, 약한 참조는 해당 인스턴스가 할당 해제되도록 허용하며 이후 nil이 됩니다. 이는 계산이 얼마나 오래 기다리는지와 관계없이 적용되는 언어 동작입니다. 자동 참조 카운팅
캡처 문법을 선택하기 전에 동작 규칙을 정하세요. 다음 결정 표는 지연되는 데스크톱 미리보기에 이러한 언어 규칙을 적용합니다.
| 원하는 동작 | 캡처 전략 | 호출자가 소유 객체에 대한 참조를 해제한 뒤에는 어떻게 될까요? |
|---|---|---|
| 같은 소유 객체를 사용해 완료 | [self] |
작업은 처리에 필요한 소유 객체를 계속 사용할 수 있습니다. |
| 소유 객체를 사용할 수 있는 동안에만 계속 진행 | [weak self] |
기다린 뒤 작업은 소유 객체가 아직 존재하는지 확인합니다. |
| 예약할 때 선택한 값을 사용해 완료 | [samples = self.samples] |
작업은 캡처한 값을 유지하지만 이 소유 객체는 유지하지 않습니다. |
스냅샷 행에는 중요한 조건이 있습니다. 이 예제는 정수 배열을 캡처합니다. 캡처 목록 표현식은 클로저가 생성될 때 평가됩니다. 대신 클래스 인스턴스를 캡처하면 해당 인스턴스에 대한 참조를 유지할 뿐, 변경 가능한 내용의 독립적인 복사본을 만들지는 않습니다. 표현식
값에 관한 별도의 고려 사항도 있습니다. 아래의 강한 캡처와 약한 캡처 버전은 지연 후 소유 객체의 값을 읽습니다. 스냅샷 버전은 예약 시점의 값을 사용합니다. 기다리는 동안 미리보기 입력이 바뀔 수 있다면 결과가 어느 시점의 값을 나타내야 하는지 정하세요.
Mac에서 완전한 로컬 비교 실행하기
macOS 13 이상과 Swift 6 이상이 설치된 툴체인을 사용하세요. 여기서 사용하는 시간 기반 Task.sleep(for:) 오버로드는 macOS 13 이상용으로 문서화되어 있습니다. 이 오버로드는 기본 스레드를 차단하지 않고 작업을 일시 중단하며, 대기가 끝나기 전에 취소되면 CancellationError를 던집니다. sleep(for:tolerance:clock:)
다음 전체 프로그램을 CaptureDemo.swift로 저장하세요. 계산기와 작업 클로저, 데모는 모두 @MainActor에서 실행되므로 상태 접근이 직렬화됩니다. Swift 문서에는 클래스 수준의 메인 액터 격리와 여기서 사용한 명시적 클로저 주석이 모두 설명되어 있습니다. 동시성
import Foundation
enum CapturePolicy: String {
case strong, weak, snapshot
}
@MainActor
func formatTotal(_ samples: [Int]) -> String {
let total = samples.reduce(0, +)
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.usesGroupingSeparator = false
formatter.maximumFractionDigits = 0
guard let text = formatter.string(from: NSNumber(value: total)) else {
return "Formatting unavailable"
}
return "Total: \(text)"
}
@MainActor
final class Calculator {
var samples = [4, 8, 12]
func start(_ policy: CapturePolicy) -> Task<String, Error> {
switch policy {
case .strong:
return Task { @MainActor [self] in
try await Task.sleep(for: .milliseconds(300))
try Task.checkCancellation()
return formatTotal(self.samples)
}
case .weak:
return Task { @MainActor [weak self] in
try await Task.sleep(for: .milliseconds(300))
try Task.checkCancellation()
guard let self else {
return "Owner unavailable"
}
return formatTotal(self.samples)
}
case .snapshot:
return Task { @MainActor [samples = self.samples] in
try await Task.sleep(for: .milliseconds(300))
try Task.checkCancellation()
return formatTotal(samples)
}
}
}
}
@main
struct CaptureDemo {
@MainActor
static func demonstrate(
_ policy: CapturePolicy,
cancel: Bool = false
) async {
var owner: Calculator? = Calculator()
weak var probe = owner
let task = owner!.start(policy)
owner = nil
let label = cancel ? "\(policy.rawValue), cancelled" : policy.rawValue
print("\(label) retains owner: \(probe != nil)")
if cancel {
task.cancel()
}
do {
let result = try await task.value
print(result)
} catch is CancellationError {
print("Cancelled")
} catch {
print("Calculation failed: \(error)")
}
}
@MainActor
static func main() async {
await demonstrate(.strong)
await demonstrate(.weak)
await demonstrate(.snapshot)
await demonstrate(.snapshot, cancel: true)
}
}
산술 연산에는 표준 라이브러리의 reduce(0, +)을 사용해 정수를 하나의 합계로 결합합니다. Foundation의 NumberFormatter는 합계를 화면에 표시할 문자열로 바꾸며, string(from:) 메서드는 선택적 문자열을 반환하므로 프로그램은 서식화 실패를 명시적으로 알립니다. 고정된 샘플의 합계는 24입니다. reduce(::), NumberFormatter, string(from:)
터미널에서 파일이 있는 폴더로 이동한 뒤 다음 명령을 실행하세요. 대상은 Mac 프로세서 아키텍처용 macOS 13을 지정합니다. -parse-as-library 옵션을 사용하면 파일에 명시적인 @main 진입점을 둘 수 있습니다. Swift 컴파일러는 소스 파일과 명령줄 옵션을 받으며, swiftc -help를 실행하면 설치된 툴체인의 옵션을 확인할 수 있습니다. Swift 명령줄 설명서
swift --version
swiftc -parse-as-library -swift-version 6 -target "$(uname -m)-apple-macos13.0" CaptureDemo.swift -o CaptureDemo
./CaptureDemo
예상되는 애플리케이션 출력은 다음과 같습니다. 캡처 및 취소 규칙을 바탕으로 도출한 결과입니다. 이 원고를 조사한 환경에는 Swift 컴파일러가 없어 프로그램을 검토만 했으며 여기서 실행하지는 않았습니다.
strong retains owner: true
Total: 24
weak retains owner: false
Owner unavailable
snapshot retains owner: false
Total: 24
snapshot, cancelled retains owner: false
Cancelled
약한 probe은 강한 참조를 추가하지 않고 소유 여부를 확인합니다. 각 데모는 작업을 기다리기 전에 자체 강한 참조를 해제합니다. 데모와 작업 클로저가 같은 액터를 사용하므로 새로 만든 클로저는 이 동기 설정 과정에 끼어들 수 없습니다. 따라서 작업이 얼마나 빨리 시작되는지 추측하지 않고도 비교할 수 있습니다. 동시성
약한 참조를 확인하는 위치가 왜 중요할까요?
약한 참조 분기에서는 guard let self가 지연 이후에 나옵니다. 따라서 작업은 소유 객체를 강하게 유지하지 않고 기다릴 수 있습니다. 소유 객체가 사라졌다면 명시적인 Owner unavailable 결과를 내고 계산을 종료합니다.
지연 전에 언래핑하면 수명이 달라집니다. 다음은 약한 참조 분기의 클로저 본문을 대체하는 예시 조각이며, 별도의 프로그램은 아닙니다.
// Inside the existing [weak self] task closure:
guard let self else {
return "Owner unavailable"
}
try await Task.sleep(for: .milliseconds(300))
try Task.checkCancellation()
return formatTotal(self.samples)
여기서는 옵셔널 바인딩에 성공하면 일시 중단 이후에도 다시 필요한 강한 지역 참조가 만들어집니다. 따라서 바인딩이 성공한 뒤에는 대기 중에도 소유 객체를 계속 사용할 수 있습니다. 이는 Swift의 강한 참조 규칙에 따른 것입니다. 약한 캡처는 클로저가 처음 소유 객체에 접근하는 방식을 결정하고, 언래핑된 지역 변수는 이후 코드가 객체를 유지하는 방식을 결정합니다. 자동 참조 카운팅
“소유 객체를 유지하지 않고 기다린 뒤, 여전히 존재할 때만 계산”이라는 규칙을 원한다면 언래핑은 대기 뒤에 두고 곧바로 간단한 계산을 수행하세요. 언래핑 이후에 추가로 일시 중단할 지점이 있다면 수명을 다시 결정해야 합니다.
unowned 캡처는 참조된 인스턴스에 접근할 때마다 해당 인스턴스가 유효해야 합니다. 이 예제처럼 지연 중 소유 객체를 해제하는 것이 의도된 경우에 필요한 옵셔널 부재 확인을 제공하지 않습니다. 자동 참조 카운팅
작업 핸들을 해제하면 계산이 중지되나요?
핸들을 버려도 작업은 계속 실행됩니다. 핸들을 유지하면 결과를 기다리거나 취소를 요청할 수 있습니다. Apple 문서에 따르면 작업이 완료되면 작업 클로저와 그 클로저가 보유한 참조를 즉시 해제합니다. 따라서 유한한 강한 캡처는 작업이 진행되는 동안 소유 객체의 수명을 연장하지만, 지속적인 참조 순환을 반드시 만드는 것은 아닙니다. Task
취소는 소유 여부와 별개의 규칙입니다. 마지막 데모는 핸들을 통해 취소를 요청합니다. 던지는 절전 함수와 뒤이은 Task.checkCancellation()은 작업을 중지할 수 있는 지점을 제공하며, 호출자는 그 결과를 알립니다. Swift의 취소는 협력 방식으로 동작합니다. 취소를 요청해도 임의의 계산 코드가 강제로 중단되지는 않습니다. Task
취소 시 결과 생성을 막아야 하는데도 던지는 절전 함수를 try?로 바꾸고 무조건 진행하지 마세요. 오류를 억제하면 다른 취소 확인이 중단하지 않는 한 계산까지 실행될 수 있습니다. 마찬가지로 소유 객체의 변수에 nil을 할당하는 것도 취소 요청이 아닙니다.
데스크톱 미리보기에서는 이 차이를 통해 유용한 세 가지 제어 방법을 얻을 수 있습니다. 소유 객체를 계속 할당 상태로 둘지, 계산에 어떤 입력 값을 사용할지, 작업을 중지할지 여부입니다. 약한 캡처가 이 세 가지를 모두 처리한다고 기대하지 말고 각각 명시적으로 선택하세요.
데스크톱 미리보기에서는 무엇을 바꿔야 할까요?
대기 중인 미리보기가 소유 객체를 사용할 수 있을 때만 의미가 있다면 약한 참조 버전을 사용하세요. 선택한 합성 입력으로 독립적인 결과를 내야 한다면 스냅샷 버전을 사용하세요. 작업을 완료하는 데 소유 객체가 실제로 필요하다면 강한 참조 버전을 사용하세요.
객체의 수명은 인터페이스의 표시 여부와도 다릅니다. 창을 닫았다고 해서 계산기에 대한 모든 강한 참조가 해제되었다고 단정할 수는 없습니다. 미리보기를 닫을 때 애플리케이션이 대기 중인 작업을 중지해야 한다면, 애플리케이션 이벤트를 유지 중인 작업 핸들의 cancel() 메서드에 연결하세요. 계산 자체는 여전히 취소에 협력해야 합니다. Task
이 예제의 작업량은 적게 유지하세요. 메인 액터 격리는 상태 접근을 단순하게 하고 대기 중에는 작업이 일시 중단되지만, 동기 합계 계산은 여전히 해당 액터에서 실행됩니다. Swift 동시성 안내서에 따르면 메인 액터는 인터페이스 작업도 처리합니다. 계산량이 훨씬 많다면 적절한 값을 작업자에게 전달하는 별도의 실행 설계가 필요합니다. 동시성
이 로컬 계산에서는 결정적인 확인 방법이 간단합니다. 지연된 작업이 실행되기 전에 호출자의 참조를 제거하고 결과를 관찰하세요. 강한 캡처는 소유 객체를 유지하고, 약한 캡처는 명시적으로 객체를 사용할 수 없는 결과를 허용하며, 정수 배열 스냅샷은 완료에 필요한 입력만 유지합니다.
출처
- 자동 참조 카운팅 — Swift.org
- 표현식 — Swift.org
- 동시성 — Swift.org
- Task — Apple Developer
- sleep(for:tolerance:clock:) — Apple Developer
- reduce(::) — Apple Developer
- NumberFormatter — Apple Developer
- string(from:) — Apple Developer
- Swift 명령줄 설명서 — Swift project