Swift에서 ARC 최적화: weak vs unowned
iOS 앱에서 메모리 누수와 크래시는 대부분 소유권(ownership) 을 잘못 설계할 때 발생합니다. Swift의 ARC는 자동으로 메모리를 관리하지만, 순환 참조(retain cycle) 를 개발자가 끊어줘야 합니다. 이 문서는 weak 과 unowned 를 언제, 왜, 어떻게 써야 하는지 안전성·성능·실무 패턴까지 자세히 정리합니다.
1. 핵심 요약
| 항목 | weak | unowned |
| 소유권 | 비소유(non‑owning) | 비소유(non‑owning) |
| 옵셔널 여부 | 항상 Optional (자동 nil세팅) | Non‑Optional 가능 (nil 아님 가정) |
| 해제 시 동작 | 대상이 해제되면 자동으로 nil | 대상이 해제되면 댕글링 포인터 → 접근 시 크래시 |
| 런타임 비용 | 제로잉(Zeroing) 테이블 관리 비용 존재 | 비교적 낮음 (제로잉 없음) |
| 안전성 | 가장 안전 | 가장 빠르지만 위험 |
| 권장 용도 | delegate, back‑reference, 캡처리스트 기본값 | 대상의 수명이 더 길거나 정확히 동등임을 증명할 수 있을 때만 |
2. ARC 기본 동작 간단 복습
- strong: 참조 카운트(retain) 증가 → 소유권 보유
- weak: retain 증가하지 않음, 대상 해제 시 자동 nil
- unowned: retain 증가하지 않음, 대상 해제 시 nil 아님(접근하면 크래시)
final class Owner {
var child: Child? // strong
}
final class Child {
weak var owner: Owner? // back‑reference는 weak가 기본
}
원칙: 트리 구조는 부모→자식 strong, 자식→부모 weak 로 순환을 끊습니다.
3. weak 와 unowned 의 선택 기준
3.1 실무 체크리스트
- 대상 수명이 나보다 길다 또는 항상 함께 산다고 증명 가능? → unowned 고려
- 그 외에는 기본 weak. 특히 비동기, 타이머, 콜백, UI 이벤트는 수명 추적이 어려우므로 weak이 안전
3.2 대표 사례
- delegate: weak (Apple 프레임워크 관례)
- 클로저 캡처리스트: 기본 [weak self]; 짧고 동기적이며 self가 반드시 살아있음이 명백하면 [unowned self] 가능
- 뷰컨↔뷰모델(양방향): 한쪽은 weak
- 오래 사는 매니저 → 일시적인 소유자: 역참조는 weak
4. 캡처리스트와 retain cycle
4.1 순환 참조 발생 예
final class VC: UIViewController {
var work: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
work = {
// self 캡처 → VC가 work을 strong으로 잡고, work가 self를 잡아 cycle
print(self.view.bounds)
}
}
}
4.2 해결: [weak self] 패턴
work = { [weak self] in
guard let self else { return }
print(self.view.bounds)
}
4.3 [unowned self] 는 언제?
work = { [unowned self] in
// 이 클로저가 VC보다 **항상 짧게** 존재한다는 게 논리적으로 보장될 때만
print(view.bounds)
}
- 예: 즉시 실행되는 동기 클로저, 라이프사이클상 클로저가 VC 프로퍼티로 오래 저장되지 않는 경우
- 비동기 지연/재시도/타이머/네트워크 콜백 → 위험. self 해제 후 실행되면 크래시
5. Delegate/Owner 패턴 베스트 프랙티스
protocol PlayerDelegate: AnyObject { // AnyObject로 class‑only
func didFinish(_ player: Player)
}
final class Player {
weak var delegate: PlayerDelegate? // weak
func finish() { delegate?.didFinish(self) }
}
final class Screen: PlayerDelegate {
private let player = Player() // strong
init() { player.delegate = self } // back‑reference weak로 cycle 방지
func didFinish(_ player: Player) { /* ... */ }
}
- delegate 는 항상 weak. 그래야 Screen 이 사라질 때 cycle 없이 내려감.
6. Timer / Notification / Combine / async 패턴
6.1 Timer (Foundation)
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.tick()
}
6.2 NotificationCenter
let token = NotificationCenter.default.addObserver(
forName: .something, object: nil, queue: .main
) { [weak self] _ in
self?.handle()
}
// deinit 또는 viewWillDisappear에서 removeObserver(token)
6.3 Combine
cancellable = publisher
.sink { [weak self] value in self?.render(value) }
6.4 Swift Concurrency
task = Task { [weak self] in
guard let self else { return }
await self.load()
}
장수(長壽) 리소스의 콜백은 무조건 약하게(weak) 잡는 걸 기본값으로.
7. weak vs unowned 성능 노트
- weak 는 제로잉 weak 테이블 관리로 약간의 오버헤드가 있습니다.
- unowned 는 제로잉 없음 → 접근 비용은 더 낮음.
- 그러나 대부분의 앱에서 차이는 미미합니다. 안전성 우선 → 기본 weak.
- 진성 핫패스에서 측정으로 병목이 확인될 때만 unowned 로 전환을 검토하세요.
성능 판단은 반드시 Release + 최적화 빌드에서 측정(Instruments Time Profiler).
8. 크래시를 만드는 unowned 반례
final class Store {
var handler: (() -> Void)?
}
final class VC {
let store = Store()
func bind() {
store.handler = { [unowned self] in // ⚠️ VC 해제 후 호출되면 크래시
print(self)
}
}
}
- 네트워크 재시도/백그라운드 재개/지연 타이머 등에서 unowned 는 언젠가 터집니다.
- 이런 경우는 항상 [weak self] + 존재 확인(guard let/self?)이 정석.
9. 컬렉션/캐시와 약한 참조
9.1 약한 참조 래퍼
final class WeakBox<T: AnyObject> {
weak var value: T?
init(_ value: T) { self.value = value }
}
var listeners = [WeakBox<AnyObject>]()
func addListener(_ l: AnyObject) { listeners.append(WeakBox(l)) }
func compactListeners() { listeners.removeAll { $0.value == nil } }
9.2 NSMapTable / NSPointerArray (Obj‑C 기반)
- NSMapTable.weakToStrongObjects() 등 weak 키/값 구성이 가능
- 캐시/리스너 목록에서 순환 방지에 유용
10. deinit 로 수명 확인하기
final class Foo {
deinit { print("Foo deinit") }
}
- 디버깅 중 사라져야 할 객체가 안 사라지면 retain cycle 의심
- Xcode Memory Graph / Allocations / Leaks 도구로 추적
11. 구조적 소유권 설계 (디자인 규칙)
- 단방향 강한 소유권 + 역방향 약한 참조 (Owner→Child strong, Child→Owner weak)
- 뷰 계층/코디네이터/라우팅에도 동일 원칙 적용
- 비즈니스 그래프가 복잡하면 “핵심 데이터는 Value Type(Struct)”, UI/브리지는 Class로 구분하여 참조 그래프 축소
12. 예제: ViewController ↔ ViewModel
final class ViewModel {
weak var output: ViewModelOutput?
func load() { output?.didLoad("Hello") }
}
protocol ViewModelOutput: AnyObject { func didLoad(_ text: String) }
final class VC: UIViewController, ViewModelOutput {
private let vm = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
vm.output = self // back‑reference weak
vm.load()
}
func didLoad(_ text: String) { /* update UI */ }
}
13. 예제: 클로저 보관 프로퍼티와 캡처
final class Loader {
var onComplete: ((Data) -> Void)?
func start() { /* async ... */ }
}
final class Screen {
private let loader = Loader()
func show() {
loader.onComplete = { [weak self] data in // 기본은 weak
self?.render(data)
}
loader.start()
}
private func render(_ data: Data) { /* ... */ }
}
14. 흔한 질문(FAQ)
Q1. weak는 항상 Optional이어야 하나요?
A. 네. 해제 시 ARC가 자동으로 nil을 넣어야 하므로 weak var 는 Optional 이어야 합니다.
Q2. unowned Optional은 안 되나요?
A. 불가합니다. unowned 는 “nil이 될 수 없다”는 전제가 전부입니다. (필요하면 weak 사용)
Q3. 성능 때문에 전부 unowned로 바꾸면 안 되나요?
A. 권장하지 않습니다. 성능 차이는 대부분 미미하며, 크래시 리스크가 큽니다. 측정 기반으로 특정 지점만 고려하세요.
Q4. struct/enum 같은 값 타입에도 weak/unowned를 쓸 수 있나요?
A. 아니오. 약한 참조는 클래스 인스턴스(참조 타입)에만 의미가 있습니다.
Q5. 클로저에서 [weak self] 후 강제 언래핑(!)을 써도 되나요?
A. 지양하세요. guard let self else { return } 또는 if let self = self { ... } 로 처리하세요.
15. 선택 가이드 (요약 표)
| 상황 | 권장 선택 | 근거 |
| delegate/back‑reference | weak | 관례 + 자동 nil |
| 비동기 콜백/타이머/Notification/Combine | weak | 수명 예측 어려움 |
| 즉시 실행 동기 클로저(수명 확실) | unowned 가능 | 빠르고 간단 |
| Owner→Child | strong | 소유권 명확화 |
| Child→Owner | weak | cycle 제거 |
| 극한 성능 핫패스 | 측정 후 unowned검토 | 오버헤드 감소 |
16. 점검 체크리스트
- 캡처리스트에서 기본값은 [weak self]
- Owner→Child strong / Child→Owner weak 설계
- 비동기/지연 콜백에서 unowned 금지
- 의도적으로 unowned 를 썼다면 수명 관계를 코드/주석으로 명확히
- Instruments(Time Profiler, Leaks, Memory Graph)로 주기적 점검
17. 샘플 코드 (디버그용)
final class A { deinit { print("A deinit") } }
final class B {
weak var a: A? // unowned로 바꿔가며 비교
init(a: A) { self.a = a }
}
func demo() {
var a: A? = A()
var b: B? = B(a: a!)
a = nil // weak이면 b?.a == nil, unowned면 이후 접근 시 크래시
print(b?.a as Any)
b = nil
}
demo()
18. 결론
- weak 은 안전이 최우선인 기본값, unowned 는 증명된 수명 관계에서만 제한적으로 사용하세요.
- 성능 최적화는 측정 기반으로, 크래시 리스크를 감수하면서까지 unowned 를 남용하지 마세요.
- 소유권 그래프를 단방향 strong + 역방향 weak 으로 설계하면 대부분의 순환 참조 문제를 예방할 수 있습니다.
'Dev Study > Swift' 카테고리의 다른 글
| Swift에서 정렬: sort vs sorted (1) | 2025.11.11 |
|---|---|
| Swift에서 옵셔널 기본값: 중첩 if let vs a ?? b (0) | 2025.11.11 |
| Swift에서 배열 초기화: 반복 append vs Array(repeating:count:) (0) | 2025.11.11 |
| Swift에서 문자열 비교 (0) | 2025.11.11 |
| Swift에서 @inlinable / @inline(__always) (0) | 2025.11.11 |
| Swift에서 구조체(Value Type) 기반 설계 (0) | 2025.11.11 |
| Swift에서 switch문의 pattern matching (0) | 2025.11.11 |
| Swift에서 문자열 결합을 위해 joined(separator:) 사용하기 (0) | 2025.11.11 |


