Swift - Automatic Reference Counting(ARC) 2


Strong Reference Cycles for Closures

강한 참조 순환 문제는 클로저를 클래스 인스턴스의 프로퍼티로 선언하고, 해당 클로저가 인스턴스를 캡쳐할 때도 발생한다. 이러한 강한 참조 순환은 클로저가 클래스처럼 참조 타입이기 때문에 발생한다. 본질적으로는 두 클래스의 인스턴스가 서로를 강한 참조하는 것과 같은 문제지만, 인스턴스 사이의 관계와 달리, 클로저의 경우 여전히 살아있으면서 서로 참조를 유지하는 것이다. Swift는 이러한 문제를 위해 클로저 캡쳐 리스트(closure capture list)라는 방법을 제공한다. 먼저, 클로저 캡쳐 리스트를 이용해 문제를 해결하는 방법을 알아보기 전에 어떻게 참조 순환이 발생하는지 부터 알아보자.

예제 코드:

클로저에서 self 를 참조하면서 발생하는 강한 참조 순환의 예.

class HTMLElement {
    let name: String
    let text: String?

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

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }

}

HTMLElement 클래스는 ‘h1’, ‘p’, ‘br’ 같은 HTML 태그의 이름을 나타내는 name, 해당 요소 내의 텍스트를 나타내는 text 프로퍼티를 가진다. 또한 lazy 프로퍼티인 asHTML 프로퍼티도 가지고, 이 프로퍼티는 nametext를 합치는 클로저를 참조한다. asHTML 프로퍼티는 클로저 프로퍼티라는 특성을 활용해 커스텀 클로저로 값을 대체할 수도 있다.

예를 들어 asHTML 프로퍼티는 text 프로퍼티가 nil인 경우 빈 HTML 태그를 반환하지 않도록 디폴트 텍스트를 설정할 수 있는 클로저로 변경할 수 있다.

let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
    return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// Prints "<h1>some default text</h1>"

HTMLElement 클래스 인스턴스를 생성하고 출력하는 것은 다음과 같다.

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"

위에서 본 코드는 다음과 같은 형태로 인스턴스와 클로저 사이의 강한 참조 순환을 유발한다.

closureReferenceCycle01_2x

인스턴스의 asHTML 프로퍼티는 클로저를 강한 참조 하고, 클로저 역시 내부에서 self를 캡쳐한다. 이로 인해 인스턴스에 대한 강한 참조가 발생하게 된다.

paragraph = nil

만약 paragraph 변수에 nil을 할당하여 HTMLElement 인스턴스에 대한 강한 참조를 없애도, HTMLElement 인스턴스와 클로저는 메모리에서 해제되지 않는다. 따라서 deinit 메시지도 출력되지 않는 것을 볼 수 있다.

Resolving Strong Reference Cycles for Closures

클로저와 클래스 인스턴스 사이의 강한 참조 순환 문제는 캡쳐 리스트를 통해 해결할 수 있다. 캡쳐 리스트는 하나 이상의 참조 타입을 클로저 내부에서 캡쳐할 때 적용하는 규칙을 정의한다. 캡쳐된 참조를 강한 참조 대신 약한참조와 미소유 참조로 선언할 수 있다.

Defining a Capture List

캡쳐 리스트의 각각의 요소는 weak, unowned 키워드와 참조하는 클래스 인스턴스 혹은 변수와 함께 짝을 이루어 선언된다.

클로저의 파라미터 리스트와 반환 타입 전에 캡쳐 리스트가 위치한다.

lazy var someClosure = {
    [unowned self, weak delegate = self.delegate]
    (index: Int, stringToProcess: String) -> String in
    // closure body goes here
}

만약 클로저가 파라미터 리스트 혹은 반환 타입을 명시하지 않는다면 캡쳐 리스트를 클로저의 시작 부분에 위치하면 되며, in 키워드도 작성해야 한다,

lazy var someClosure = {
    [unowned self, weak delegate = self.delegate] in
    // closure body goes here
}

Weak and Unowned References

클로저와 클로저가 캡쳐하는 인스턴스가 언제나 서로를 참조하고 언제나 메모리에서 동시에 해제된다면 클로저의 캡쳐를 unowned로 정의해야 한다. 반대로, 캡쳐된 참조가 후에 어느 시점에 nil이 될 수 있다면 weak로 정의해야 한다. 약한 참조는 언제나 옵셔널 타입이고 인스턴스가 해제될 때 자동으로 nil이 된다.

미소유 참조는 위에서 본 HTMLElement 예제에서 발생하는 강한 참조 순환을 해결할 수 있다.

class HTMLElement {

    let name: String
    let text: String?

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

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }

}

클로저 내부에 캡쳐 리스트를 추가한 것을 제외하면 HTMLElement는 위에서 본 것과 완전히 동일하다. 캡쳐 리스트는 [unowned self]이고, 이는 self를 강한 참조하지 않고 미소유 참조한다는 뜻이다.

이전과 같이 인스턴스를 생성해보자

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"

클로저와 인스턴스의 관계:

closureReferenceCycle02_2x

클로저의 self 캡쳐는 미소유 참조를 하기 때문에 클로저는 인스턴스를 강한 참조하지 않게되고, paragraph 변수에 nil을 할당하면, 인스턴스는 메모리에서 해제되고 deinit 메시지가 출력되는 것을 볼 수 있다.

paragraph = nil
// Prints "p is being deinitialized" 

Reference

공식 Swift 문서

Tags:

Categories:

Updated: