본문 바로가기

iOS 개발/App 개발 관련

[iOS] 강한 참조 순환(Retain Cycle), 참조 순환 해결하기

Swift Document - Automatic Reference Counting

 

클래스 객체 간의 강한 참조 순환 (Strong Reference Cycle, Retain Cycle)

두 객체가 서로에 대한 참조를 멤버로 가진 상태에서 각 객체에 대한 참조가 모두 할당 해제 되어도 서로를 참조하는 멤버의 참조는 여전히 유효하기 때문에 접근하지도 못하는 두 객체가 메모리에 남아있게 되는 메모리 누수 현상이다. 

 

두 객체가 메모리에 남는 이유는 Swift 의 ARC 즉, 자동 참조 카운트 때문인데 Swift 는 어떤 객체가 누군가에 의해 참조되고 있음을 카운팅하고 이것이 0 이 되었을 때 메모리에서 해제한다. 따라서 두 객체의 멤버가 서로를 참조하기 때문에 메모리에서 해제되지 않는다.

객체 간의 참조 순환 해결하기

Swift 에서 참조를 갖는다는 것은 기본적으로 '강한 참조' 를 의미한다. 참조 순환을 피하기 위해 이를 '강하지 않은 참조' 로 선언하면 된다. 이러한 선언에는 weak, unowned 두 가지의 선언이 있다.

weak

참조 선언에 weak 키워드를 추가하면 대상을 약하게 참조한다. 약한 참조이기 때문에 ARC 가 주목하는 대상이 아니게 되고 따라서 어떤 객체가 약한 참조만을 받고 있다면 해당 객체는 할당 해제된다.

 

약한 참조의 대상은 해당 참조 변수를 가지는 객체보다 빨리 할당 해제되는 상황에서 사용한다. 참조 대상이 할당 해제된 경우 참조 변수의 값이 nil 로 변경되기 때문에 weak 프로퍼티는 항상 옵셔널 변수로 선언된다.

 

아래 예제에서 아파트는 세입자를 가질 수 있고 어느 시점에는 가지지 않을 수도 있으므로 tenant 라는 옵셔널 변수를 통해 Person 을 약하게 참조한다. 

 

만약 Person 과 Apartment 각 객체에 대한 참조를 해제하면 Person 에 대한 강한 참조가 존재하지 않기 때문에 먼저 할당 해제되고 이에 따라 Apartment 에 대한 강한 참조도 사라져 모두 할당 해제된다.

 

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    weak var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

unowned

참조 선언에 unowned 키워드를 추가하면 마찬가지로 대상을 강하지 않게 참조한다. 미소유 참조라고도 하는데 미소유 참조는 참조 대상이 참조하는 프로퍼티를 가지는 객체보다 오래 존재하거나 비슷할 때 사용한다. 즉, 해당 프로퍼티가 대상을 미소유 참조하는 동안 대상이 할당 해제되는 일이 없고 따라서 참조값이 nil 이 되거나 하는 일도 없다.

 

어떤 참조 프로퍼티를 선언할 때 해당 프로퍼티가 항상 참조하는 대상을 가질 것이 확실할 때 미소유 참조를 사용하면 된다. (미소유 참조의 대상이 할당 해제된 경우 그 값에 접근하려 하면 런타임 에러를 일으킨다.)

 

아래 예제에서 CreditCard 는 대응하는 Customer 가 없는 한 존재하지 않고 존재할 이유도 없다. (CreditCard 의 생성자만 확인해봐도 알 수 있는 사실이다.) 따라서 CreditCard 의 customer 프로퍼티는 상수로써 Customer 를 미소유 참조하고 있다.

 

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") }
}

클로저에서의 강한 참조 순환 (Self Reference Cycle)

클로저 선언 시 주변의 상수 혹은 변수를 캡처할 수 있다. 만약 클로저가 어떤 객체를 캡처한다면 내부에서 그 객체에 대해 strong reference 를 가진다.

 

만약 클래스 프로퍼티에 클로저를 선언하고 그 내부에서 self 키워드를 통해 클래스 인스턴스를 캡처한다면 클래스 인스턴스와 클로저 사이에 강한 참조 순환이 생긴다.

클로저와의 강한 참조 순환 해결하기

클로저와의 강한 참조 순환을 해결하는 방법은 클로저의 정의에 capture list 를 작성하는 것이다. Capture list 에는 클로저 내부에서 capture 할 참조 타입에 대한 규칙을 정할 수 있다. 여기에서 해당 참조를 unowned or weak 로 지정하여 문제를 해결할 수 있다.

unowned

만약 클래스 인스턴스와 클로저가 항상 서로를 참조하고 동시에 할당 해제 된다면 unowned 로 선언한다. unowned 참조는 옵셔널 값을 강제로 해제하기 때문에 참조 대상이 할당 해제된 후에 접근할 경우 런타임 에러를 일으킨다.

 

lazy var asHTML: () -> String = {
        [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

weak

만약 클로저가 capture 하는 참조가 미래 어느 시점에 nil 이 될 가능성이 있다면 weak 로 선언한다. 이는 참조 대상을 옵셔널 타입으로 만들고 대상이 할당 해제되면 자동으로 nil 이 된다. 따라서 클로저 내부에서 참조 대상에 접근할 때 항상 옵셔널 바인딩 과정이 필요하다.

 

let changeColorToRed = DispatchWorkItem { [weak self] in
    self?.view.backgroundColor = .red
}