ARC란? 클로저(Closures)의 순환참조 [Part-3]

Strong Reference Cycles for Closures

이번 장에서는 클로져의 순환참조에 대해 알아보겠습니다.
이전장(Part-2)에서는 두 클래스 인스턴스 프로퍼티가 서로에 대한 강한 참조를 보유 할 때 순환참조가 생성되는 방법을 살펴 보았습니다. 또한 이러한 순환참조를 깨기 위해 약한(weak) 참조와 미소유(unowned) 참조를 사용하는 방법도 보았습니다.

클로져의 순환참조를 쉽게 이해하하기 위해서는 [캡쳐 리스트]에 대해 알고 있는 것이 좋습니다.

클래스 인스턴스의 프로퍼티에 클로저를 할당하고 해당 클로저의 본문이 인스턴스를 캡처하는 경우 순환참조가 발생할 수 있습니다. 이 캡처는 클로저의 본문이 self.someProperty와 같은 인스턴스의 프로퍼티에 액세스하거나 클로저가 self.someMethod ()와 같은 인스턴스의 메서드를 호출하기 때문에 발생할 수 있습니다. 두 경우 모두 이러한 액세스로 인해 클로저가 self을 “캡처”하여 강한 순환참조를 발생시킵니다.

이 순환참조는 클로저가 클래스와 같은 레퍼런스(Reference) 유형이기 때문에 발생합니다. 프로퍼티에 클로저를 할당하면 해당 클로저에 대해 강한참조가 할당되어 집니다. 본질적으로 이전 예제들과 동일한 문제입니다. 두 개의 강한 참조가 서로를 유지하고 있습니다. 그러나 이번에는 두 개의 클래스 인스턴스가 아니라 클래스 인스턴스클로저입니다.

Swift는 클로저 캡처 리스트로 알려진 이 문제에 대한 우아한 솔루션을 제공합니다. 그러나 클로저 캡처 리스트로 순환참조를 끊는 방법을 배우기 전에 이러한 순환이 어떻게 발생할 수 있는지 이해하는 것이 좋습니다.

아래 예제는 self를 참조하는 클로저를 사용할 때 어떻게 순환참조를 발생 할 수 있는지를 보여줍니다. 이 예제에서는 HTML 문서 내의 개별 요소에 대한 간단한 모델을 제공하는 HTMLElement라는 클래스를 정의했습니다.

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”과 같이 요소의 이름을 나타내는 name 프로퍼티를 정의했습니다. HTMLElement는 또한 해당 HTML 요소 내에서 렌더링 될 텍스트를 나타내는 문자열로 설정할 수 있는 옵셔널 text 프로퍼티도 정의했습니다.

이 두 가지 간단한 프로퍼티 외에도 HTMLElement 클래스는 asHTML이라는 lazy 프로퍼티를 정의했습니다. 이 속성은 name과 text를 HTML 문자열로 결합하는 클로저를 참조합니다. asHTML 프로퍼티는 ()-> String 또는 “매개 변수를 사용하지 않고 문자열 값을 반환하는 함수“유형입니다.

기본적으로 asHTML 프로퍼티에는 HTML 태그의 문자열 표현을 반환하는 클로저가 할당되어있습니다. 이 태그에는 값이 있는경우는 옵셔널 text 값이 포함되고 값이 없는경우는 빈 텍스트 콘텐츠를 반환합니다. 단락 요소”p”의 경우 클로저는 text 프로퍼티가 “특정문자열”인지 아니면 nil인지에 따라 “<p>특정문자열</p> “또는 “<p />”를 반환합니다.

asHTML 프로퍼티는 인스턴스 메서드와 비슷하게 이름이 지정되고 사용됩니다. 그러나 asHTML은 인스턴스 메서드가 아니라 클로저 프로퍼티이므로 특정 HTML 요소에 대한 HTML 렌더링을 변경하려는 경우 asHTML 프로퍼티의 기본값을 아래의 예제 코드처럼 사용자 지정 클로저로 바꿀 수 있습니다.

예를 들어 asHTML 프로퍼티는 빈 HTML 태그를 반환하는 것을 방지하기 위해 text 프로퍼티가 nil 인 경우 일부 텍스트를 기본값으로하는 클로저로 설정할 수 있습니다.

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

NOTE
asHTML 프로퍼티는 요소가 실제로 일부 HTML 출력 대상의 문자열 값으로 렌더링되어야하는 경우에만 필요하므로 lazy*지연 속성으로 선언됩니다. asHTML이 lazy 프로퍼티라는 사실은 초기화가 완료되고 self가 존재하는 것으로 알려질 때까지 lazy 프로퍼티에 액세스 할 수 없기 때문에 기본 클로저 내에서 self를 참조 할 수 있음을 의미합니다.

HTMLElement 클래스는 name 인수와 text 인수(원하는 경우)를 사용하여 새 요소를 초기화하는 단일 이니셜 라이저를 제공합니다. 이 클래스는 또한 HTMLElement 인스턴스가 메모리 해제 될 때 표시 할 메시지를 프린트하는 deinitializer를 정의했습니다.

HTMLElement 클래스를 사용하여 새 인스턴스를 만들고 프린트하는 방법은 다음과 같습니다.

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

NOTE
위의 단락 변수는 옵셔널 HTMLElement로 정의되어 있으므로 순환참조의 존재를 보여주기 위해 아래에서 nil로 설정할 수 있습니다.

안타깝게도, 위에서 작성한 HTMLElement 클래스는 HTMLElement 인스턴스와 기본 asHTML 값에 사용되는 클로저 사이에 순환참조를 만듭니다. 흐름은 다음과 같습니다.

HTMLElement 클래스 인스턴스의 asHTML 프로퍼티는 해당 클로저에 대해 강한 참조를 생성합니다. 그러나 클로저는 (self.name 및 self.text를 참조하는 방법으로) 본문 내에서 self를 참조하므로 클로저는 self를 캡처하게 됩니다.
즉, 클로져 또한 HTMLElement 인스턴스에 대해서 강한 참조를하게 됩니다.
결국 둘 사이에 순환참조가 발생하게됩니다.
(클로저의 값 캡처에 대한 자세한 내용은 값 캡처(Capturing Values)를 참조하십시오.)

NOTE
클로저가 self를 여러 번 참조하더라도 HTMLElement 인스턴스에 대한 하나의 강력한 참조 만 캡처합니다.

이제 paragraph 변수를 nil로 설정하고 HTMLElement 인스턴스에 대한 강한 참조를 끊어도 순환참조로 인해 HTMLElement 인스턴스와 클로저가 메모리에서 해제되지 않는것입니다.

paragraph = nil

HTMLElement deinitializer의 메시지는 프린트되지 않으며 이것은 HTMLElement 인스턴스가 메모리 해제되지 않았음을 나타내는것입니다.

그러면 클로져의 순환참조를 어떻게 해결할 수 있는지 알아보겠습니다.

클로져에서 순환참조 문제 해결하기 (Resolving Strong Reference Cycles for Closures)

클로저와 클래스 인스턴스 사이에 순환참조는 클로져의 일부로 캡처 리스트를 정의하여 해결할 수 있습니다. 캡처 리스트는 클로저 본문 내에서 하나 이상의 참조 유형을 캡처 할 때 사용할 규칙을 정의하는것입니다. 두 클래스 인스턴스 간의 순환참조와 마찬가지로 캡처 된 각 참조를 강한 참조가 아닌 약한(weak)참조나 미소유(unowned) 참조로 선언합니다. 약한 참조 또는 미소유참조를 적절히 선택하는 것은 해당 코드 간의 관계에 따라 다릅니다.

NOTE
Swift에서는 클로저 내에서 self의 멤버를 참조 할 때마다 self.someProperty 또는 self.someMethod ()를 작성해야합니다 (someProperty 또는 someMethod ()가 아닌). 이것은 실수로 self을 캡쳐 할 수 있다는 것을 기억하는 데 도움이됩니다.

캡처 리스트 정의(Defining a Capture List)

캡처 리스트의 각 항목은 클래스 인스턴스 (예 : self) 또는 일부 값으로 초기화 된 변수 (예 : delegate = self.delegate)에 대한 참조가있는 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)

클로저와 캡처되는 인스턴스가 항상 서로를 참조하고 항상 동시에 메모리에서 해될 때 클로저의 캡처를 미소유 참조로 정의합니다.

반대로 캡처 된 참조가 미래의 어느 시점에서 nil이 될 수있는 경우 캡처를 약한 참조로 정의해야 합니다. 약한 참조는 항상 옵셔널 유형이며 참조하는 인스턴스가 메모리에서 해제되면 런타임에 자동으로 nil이됩니다. 이를 통해 클로저 본문 내에 그들의 존재가 있는지 확인할 수 있습니다.

NOTE
캡처 된 참조가 nil이되지 않으면 항상 약한 참조가 아닌 미소유 참조로 캡처되어야 할것 입니다.

위의 Closure에 대한 순환참조 예제에서는 순환참조를 해결하는 데에 미소유 참조를 사용 하는 것이 적절한 캡처 방법일것입니다. 순환참조를 피하기 위해 HTMLElement 클래스를 작성하는 방법은 다음과 같습니다.

class HTMLElement {

    let name: String
    let text: String?
    //[unowned self]로 self를 캡처 리스트에 미소유참조임을 명시
    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 클래스 구현은 asHTML 클로저 내에 캡처 리스트를 추가하는 점을 제외하면 이전 구현과 동일합니다. 이 경우 캡처 리스트는 [unowned self]이며, 이는 “자신을 강한 참조가 아닌 미소유 참조로 캡처“하라는 의미합니다.

이전과 같이 HTMLElement 인스턴스를 만들고 프린트 할 수 있습니다.

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

다음은 캡처 리스트가 있을 때 참조가 어떻게 보이는지입니다.

이번에는 클로저에 의한 self 캡처는 미소유 참조이며 캡처 한 HTMLElement 인스턴스를 강력하게 유지하지 않습니다. paragraph 변수의 강한 참조를 nil로 설정하면 아래 예제에서 deinitializer 메시지를 프린트하는것을 볼 수 있듯이 HTMLElement 인스턴스는 메모리에서 해제되게 됩니다.

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

이번장에서는 클로져에서 순환참조가 발생하는 이유와 해결방법을 알아보았습니다.

정리를 해보자면
1. 클로져는 클래스와 같은 레퍼런스타입*Reference types이기 때문에 프로퍼티에 할당 될때 레퍼런스 참조가 이루어 진다.
2. 클래스 인스턴스 프로퍼티로 할당된 클로져 본문에서 해당 클래스 인스턴스의 프로퍼티 또는 메소드에 접근하기 위해 self , self.method(), self.delegate 와같은 self 를 선언하면 클로져가 할당된 클래스 인스턴스와 클로져에서 캡쳐한 self 가 서로 참조하게 되어 순환참조가 발생하는 것이다.
3. 클로져에서의 순환참조를 피하기 위해 weak 또는 unowned 키워드로 [weak self] 와 같은 캡쳐리스트를 클로져에 정의한다.
4. 모든 클로져에서 캡쳐 리스트 [weak self] , [unowned self] 등을 사용할 필요는 없다. 이유는 클래스 인스턴스가 클로져를 레퍼런스 하고 있지 않으면 레퍼런스 타입인 클로져 자체에 강한 참조가 없는 상태이기 때문에 순환참조가 일어나지 않는다.

캡처 리스트에 대한 자세한 내용은 캡처 리스트(Capture Lists) 링크를 참조하세요.

에 발행했습니다
iOS(으)로 분류되었습니다

aaron님이 작성

아무것도 안해도 시간은 흐른다.

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다