Swift에서 클래스 간 강한 참조와 클로저 캡처로 인해 생기는 순환 참조(retain cycle)를 재현하고, 원인 분석과 해결 방법(weak/unowned)을 정리했습니다.
A와 B를 nil로 설정했는데도 deinit가 호출되지 않음 final class A {
var b: B?
deinit { #logDebug("A deinit") }
}
final class B {
var a: A?
var closure: (() -> Void)? // 클로저 기반 순환 참조 추가 예정
deinit { #logDebug("B deinit") }
}
func leakDemo() {
var a: A? = A()
var b: B? = B()
a?.b = b // A → B (강한 참조)
b?.a = a // B → A (강한 참조)
// 클로저가 A를 강하게 캡처 → B → closure → A 고리 형성
b?.closure = {
#logDebug("uses A: \(String(describing: a))")
}
a = nil
b = nil
// ❌ "A deinit", "B deinit" 출력되지 않음 (누수)
}
원인
closure가 외부 변수 a를 강하게 캡처 → 고리 강화weakfinal class A2 {
weak var b: B2? // 한쪽을 weak로
deinit { #logDebug("A2 deinit") }
}
final class B2 {
var a: A2?
var closure: (() -> Void)?
deinit { #logDebug("B2 deinit") }
}
func fixedDemo_weak_reference() {
var a: A2? = A2()
var b: B2? = B2()
a?.b = b
b?.a = a
// 클로저는 아래 B안에서 따로 처리 (B의 클로저 섹션 참고)
a = nil
b = nil
// ✅ "A2 deinit", "B2 deinit" 정상 출력
}
[weak a] 또는 [unowned a]weakfunc fixedDemo_closure_weak() {
var a: A? = A()
var b: B? = B()
a?.b = b
b?.a = a
b?.closure = { [weak a] in
guard let a else {
#logDebug("A already deallocated")
return
}
#logDebug("uses A safely: \(a)")
}
b?.closure?() // A가 살아있다면 동작
a = nil
b?.closure?() // "A already deallocated"
b = nil
// ✅ 정상 해제
}
unowned
unowned는 대상이 반드시 살아있다는 보장이 있을 때만 사용. 보장 깨지면 런타임 크래시!
func fixedDemo_closure_unowned() {
var a: A? = A()
var b: B? = B()
a?.b = b
b?.a = a
b?.closure = { [unowned a] in
// a는 살아있다고 가정
#logDebug("uses A (unowned): \(a!)")
}
b?.closure?()
// 아래 순서에서 a를 먼저 nil로 만들고 다시 closure를 호출하면 크래시!
a = nil
b = nil
}
weak vs unowned 선택 가이드| 기준 | weak | unowned |
|---|---|---|
| 참조 방식 | Optional(자동 nil) | Non-optional |
| 안전성 | 높음 (nil 체크 필요) | 낮음 (해제 시 크래시) |
| 성능/간결성 | 약간 비용 있지만 안전 | 약간 빠르고 간결 |
| 사용 조건 | 생명주기 보장 불가 | 생명주기 보장 확실 |
권장: 보통은 weak + guard let 패턴을 기본값으로,
성능/보장이 확실한 경우에만 unowned.
deinit { #logDebug("... deinit") } 추가해서 해제 시점 확인weak 로 변경했는가? [weak self]/[weak a] 또는 [unowned ...]를 적용했는가? unowned 사용 시 생명주기 보장을 명확히 했는가? (문서/주석 포함) deinit 로그가 정상 출력되는지 확인했는가? Before (문제)
b?.closure = {
#logDebug(a!) // 강한 캡처 + 강제 언래핑 → 누수/크래시 위험
}
After (해결: weak)
b?.closure = { [weak a] in
guard let a else { return }
#logDebug(a)
}
After (해결: unowned, 보장 시)
b?.closure = { [unowned a] in
#logDebug(a) // a의 생존이 반드시 보장되어야 함
}
Q. weak는 왜 Optional이어야 하나요?
A. 대상이 해제되면 ARC가 자동으로 nil을 대입하기 때문에 Optional이어야 합니다.
Q. unowned는 언제 쓰나요?
A. 캡처한 대상이 클로저보다 오래 살거나 동일한 생명주기임이 확실할 때만 권장됩니다.
Q. 클래스 대신 struct를 쓰면 괜찮나요?
A. struct는 값 타입이라 ARC 관리를 받지 않아 참조 고리 문제가 없습니다. 하지만 참조语가 필요한 경우엔 클래스를 써야 합니다.