티스토리 뷰

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
링크
«   2025/08   »
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
글 보관함