서론
- Swift에서는 메모리 관리를 ARC(Automatic Reference Counting)을 통해 개발자가 개입하지 않아도 자동으로 처리해줍니다.
- 하지만 메모리 누수(Memory leaks)나 강한 순환 참조(Strong Reference Cycles)를 방지하기 위해서는 두 가지 참조 방식인 weak, unowned는 필수로 알고 있어야합니다.
- 이번 글에서는 두 개의 차이점과 어느 시점에 적절하게 사용되어야하는지 알아볼 것입니다.
Weak
- weak 참조는 참조하는 대상의 Reference Count를 증가 시키지 않습니다. 그러므로 강한 순환 참조를 방지할 수 있게됩니다.
- weak 참조는 항상 옵셔널이어야 합니다. 만약 weak가 참조하고 있는 Reference Count가 0이 된다면, weak 변수는 nil이 되기 때문입니다.
class Department {
var manager: Employee?
deinit {
print("Department is being deinitialized")
}
}
class Employee {
weak var department: Department?
deinit {
print("Employee is being deinitialized")
}
}
var department: Department? = Department()
var manager: Employee? = Employee()
department?.manager = manager
manager?.department = department
department = nil
manager = nil
- Employee안에 department의 weak가 없다면 강한 순환 참조가 유지되어 메모리가 해제되지 않습니다.
- manager = nil이 되면, Department의 RC값이 0이 되면서 department와 manager간의 강한 순환 참조를 피할 수 있게 됩니다.
- 이 방식은 프로토콜로서 Delegate 패턴을 사용할 때 많이 사용하는 방법입니다.
클로저에서의 weak의 사용
- Escaping Closure에서 weak의 사용은 종종 있는 법입니다. 특히 캡처 리스트와 함께 [weak self]를 많이 사용합니다.
- 특히 weak 참조는 메모리 누수를 위해 클로저에 자주 사용됩니다. 우리가 자주 겪는 상황으로 예시를 들어보겠습니다.
class ViewController: UIViewController {
var dataLoader: DataLoader?
func fetchData() {
dataLoader?.loadData(completion: { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let data):
self.updateUI(with: data)
case .failure(let error):
self.showErrorMessage(error)
}
})
}
private func updateUI(with data: Data) {
// Update user interface
}
private func showErrorMessage(_ error: Error) {
// Show error message
}
}
class DataLoader {
func loadData(completion: @escaping (Result<Data, Error>) -> Void) {
// Code to load data
}
}
- 위 상황은 ViewController와 DataLoader라는 두 개의 클래스가 존재합니다.
- ViewController는 DataLoader의 loadData를 호출하여 결과 값을 @escaping 클로저를 통해 데이터를 받게 됩니다.
- 클로저 안에 있는 [weak self]는 강한 순환 참조를 방지하기 위한 장치입니다. 여기서 self는 ViewController를 의미합니다.
- guard let self = self else { return } 은 weak로 정의한 self를 옵셔널 해제를 시켜서 안전하게 접근하려는 의도입니다. 만약 ViewController가 클로저가 실행되기 전에 사라진다면, self는 nil이 되어서 로직은 실행되지 않을 것입니다.
unowned
- unowned는 겉보기에는 weak와 비슷하게 작동한다고 생각할 수 있습니다. 실제로 그것이 맞습니다.
- 하지만 여기에는 치명적인 차이점이 있습니다.
- 바로 unowned는 Optional처리가 되지 않으며, 이것은 즉 nil처리가 될 수 없음을 의미합니다. 따라서 unowned는 let(상수)로 작동합니다.
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
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
- 만약 John을 nil처리를 하면, John is being deinitialized가 불리고, Card is being deinitialized가 불립니다.
- unowned 처리를 해둬 똑같이 강한 순환 참조를 방지할 수 있는 것을 의미합니다.
결론
- 하지만 unowned는 앞서 말했듯이 unowned가 참조하고 있는 인스턴스가 없어지면, Crash가 날 수 밖에 없습니다.
- 반면에 weak는 Optional이라서 그것 자체로 Crash를 방지할 수 있습니다.
- 이것이 둘의 큰 차이점이라고 할 수 있습니다.
- 만약 절대 nil이 되지 않는 상황이라면, unowned를 사용하는 것이 더 나은 방법이지만 그렇지 않으면 weak를 쓰는 것이 낫습니다.
- 즉 unowned를 쓰는 것은 로직이 정확히 어떻게 동작하는지 100% 알고있고, 객체간의 해제 시점도 명확하게 알고 있어야 합니다