티스토리 뷰
Swift에서 메모리 관리는 ARC(Automatic Reference Counting)를 통해 자동으로 이루어지지만, 순환 참조(retain cycle) 문제를 해결하기 위해서는 개발자가 직접 weak와 unowned 참조를 적절히 사용해야 합니다. 이 두 키워드의 차이점과 사용 기준을 알아보겠습니다.
ARC와 순환 참조 문제
먼저 왜 weak와 unowned가 필요한지 이해해보겠습니다.
class Person {
let name: String
var apartment: Apartment?
init(name: String) {
self.name = name
}
deinit {
print("\(name) is being deinitialized")
}
}
class Apartment {
let unit: String
var tenant: Person?
init(unit: String) {
self.unit = unit
}
deinit {
print("Apartment \(unit) is being deinitialized")
}
}
// 순환 참조 발생
var john: Person? = Person(name: "John")
var unit4A: Apartment? = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
john = nil
unit4A = nil
// deinit이 호출되지 않음 - 메모리 누수!
위 코드에서 Person과 Apartment가 서로를 강하게 참조하여 순환 참조가 발생합니다.
weak 참조의 특징
기본 개념
- 약한 참조(weak reference)로 참조 카운트를 증가시키지 않음
- 참조하는 객체가 해제되면 자동으로 nil이 됨
- 반드시 var로 선언해야 하며, 옵셔널 타입이어야 함
사용 예시
class Person {
let name: String
var apartment: Apartment?
init(name: String) {
self.name = name
}
deinit {
print("\(name) is being deinitialized")
}
}
class Apartment {
let unit: String
weak var tenant: Person? // weak 참조
init(unit: String) {
self.unit = unit
}
deinit {
print("Apartment \(unit) is being deinitialized")
}
}
var john: Person? = Person(name: "John")
var unit4A: Apartment? = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
john = nil
// "John is being deinitialized" 출력
print(unit4A!.tenant) // nil
unit4A = nil
// "Apartment 4A is being deinitialized" 출력
weak 참조의 장점
- 안전성: 참조하는 객체가 해제되면 자동으로 nil이 되어 댕글링 포인터 문제 방지
- 유연성: 참조하는 객체의 생명주기를 예측하기 어려운 상황에서 안전하게 사용 가능
unowned 참조의 특징
기본 개념
- 소유하지 않는 참조(unowned reference)로 참조 카운트를 증가시키지 않음
- 참조하는 객체가 항상 존재한다고 가정
- 옵셔널이 아니며, 참조하는 객체가 해제된 후 접근하면 런타임 에러 발생
사용 예시
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit {
print("\(name) is being deinitialized")
}
}
class CreditCard {
let number: UInt64
unowned let customer: Customer // unowned 참조
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit {
print("Card #\(number) is being deinitialized")
}
}
var john: Customer? = Customer(name: "John")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
john = nil
// "Card #1234567890123456 is being deinitialized"
// "John is being deinitialized"
unowned 참조의 장점
- 성능: 옵셔널 체크가 필요 없어 약간의 성능 이점
- 간결성: !나 옵셔널 바인딩 없이 바로 사용 가능
주요 차이점 비교
특징 | weak | unowned |
옵셔널 여부 | 반드시 옵셔널 | 옵셔널 아님 |
선언 키워드 | var만 가능 | let, var 모두 가능 |
참조 객체 해제 시 | 자동으로 nil | 댕글링 포인터 상태 |
안전성 | 높음 | 낮음 (런타임 에러 가능) |
성능 | 약간 느림 | 약간 빠름 |
사용 편의성 | 옵셔널 처리 필요 | 바로 사용 가능 |
사용 기준과 가이드라인
weak를 사용해야 하는 경우
1. delegate 패턴
protocol TableViewDelegate: AnyObject {
func didSelectRow(at index: Int)
}
class TableView {
weak var delegate: TableViewDelegate?
}
2. parent-child 관계에서 child가 parent를 참조할 때
class Parent {
var children: [Child] = []
}
class Child {
weak var parent: Parent?
}
3. 참조하는 객체의 생명주기가 불확실한 경우
class NotificationCenter {
weak var observer: Observer?
}
unowned를 사용해야 하는 경우
1. 생명주기가 동일하거나 참조하는 객체가 더 오래 살아있는 경우
class Country {
let name: String
let capitalCity: City
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
2. 클로저에서 self 참조 시 (약한 참조가 불필요한 경우)
class DataManager {
func loadData() {
// 주의: 실무에서는 [weak self]가 더 안전함
APIClient.request { [unowned self] result in
self.handleResult(result) // 객체가 해제되면 크래시 위험
}
}
// 더 안전한 방법
func loadDataSafely() {
APIClient.request { [weak self] result in
guard let self = self else { return }
self.handleResult(result)
}
}
}
선택 기준 요약
weak를 선택하는 경우
- 참조하는 객체가 먼저 해제될 가능성이 있을 때
- 안전성이 성능보다 중요할 때
- delegate, observer 패턴 구현 시
unowned를 선택하는 경우
- 참조하는 객체가 현재 객체보다 오래 살아있음이 확실할 때
- 성능이 중요하고 안전성을 보장할 수 있을 때
- 생명주기가 강하게 연결된 객체들 간의 참조
실제 사용 시 주의사항
1. 클로저에서의 사용
class ViewController {
func setupTimer() {
// weak 사용 - 안전하지만 옵셔널 처리 필요
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateUI() // self가 nil일 수 있음
}
// unowned 사용 - 간결하지만 위험할 수 있음
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [unowned self] _ in
self.updateUI() // self가 해제되면 크래시
}
}
}
2. 옵셔널 체이닝 vs 강제 언래핑
// weak - 안전한 접근
weak var delegate: SomeDelegate?
delegate?.someMethod()
// unowned - 바로 접근 가능
unowned let owner: Owner
owner.someMethod()
성능 고려사항
일반적으로 unowned가 weak보다 약간 빠르지만, 실제 앱에서는 미미한 차이입니다. 성능보다는 안전성과 코드의 명확성을 우선으로 고려하는 것이 좋습니다.
weak와 unowned는 각각 고유한 장단점을 가지고 있습니다. 일반적으로는 안전성을 위해 weak를 사용하는 것을 권장하며, 객체의 생명주기 관계가 명확하고 성능이 중요한 경우에만 unowned를 사용하는 것이 좋습니다.
무엇보다 중요한 것은 각 상황에서 객체들의 생명주기 관계를 정확히 파악하고, 적절한 참조 타입을 선택하여 메모리 누수와 크래시를 방지하는 것입니다.
'iOS > Swift' 카테고리의 다른 글
Swift - Map / Filter / Reduce (0) | 2022.02.13 |
---|---|
Swift - Generic의 개념 (0) | 2022.02.13 |
Swift - protocol (0) | 2021.04.25 |
Swift 문법 (11) - 클로저 (Closure) 고급 (0) | 2020.12.13 |
Swift 문법 (10) - 클로저 (Closure) (0) | 2020.12.09 |
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- Neo4j
- 프로그래머스
- blendshape
- infallible
- rxswift
- Lottie
- coreml
- 카카오인턴십
- 백준온라인저지
- 알고리즘
- Swift
- Reactivex
- boj
- Swift unowned
- 코코아팟
- rxswift6
- 백준
- 안드로이드
- Swift weak
- cocoapods
- C++
- blendshapes
- ios
- disposeBag
- DispatchQueue
- Kotlin
- ARKit
- GraphDB
- SWEA
- SwiftUI
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함