메모리 관리의 weak와 unowned의 차이점

서론

  • 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% 알고있고, 객체간의 해제 시점도 명확하게 알고 있어야 합니다