🔥 클로저와 클래스 인스턴스 사이의 강한 참조 순환 해결하기

495자
6분

클로저와 클래스 인스턴스 사이의 강한 참조 순환을 해결하려면 클로저 정의의 일부로 capture list를 정의해야 해요. 캡처 리스트는 클로저 본문 내에서 하나 이상의 참조 유형을 캡처할 때 사용할 규칙을 정의하죠. 두 클래스 인스턴스 사이의 강한 참조 순환과 마찬가지로, 캡처된 각 참조를 강한 참조가 아닌 약한(weak) 참조 또는 미소유(unowned) 참조로 선언합니다. 약한 참조 또는 미소유 참조의 적절한 선택은 코드의 다른 부분 간의 관계에 따라 달라집니다.

캡처 리스트 정의

캡처 리스트의 각 항목은 weak 또는 unowned 키워드와 클래스 인스턴스에 대한 참조(예: self) 또는 일부 값으로 초기화된 변수(예: delegate = self.delegate)의 쌍이에요. 이러한 쌍은 대괄호 안에 쉼표로 구분되어 작성되죠.

캡처 리스트를 클로저의 매개변수 목록과 반환 유형 앞에 배치하면 돼요(제공된 경우):

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

클로저가 매개변수 목록이나 반환 유형을 지정하지 않는 경우(컨텍스트에서 유추할 수 있기 때문에), 캡처 리스트를 클로저의 맨 앞에 배치하고 in 키워드를 뒤에 붙이면 됩니다:

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

약한 참조와 미소유 참조

클로저와 캡처하는 인스턴스가 항상 서로를 참조하고 항상 동시에 할당 해제되는 경우, 클로저에서 캡처를 미소유(unowned) 참조로 정의하는 게 좋아요.

반대로, 캡처된 참조가 미래의 어느 시점에서 nil이 될 수 있는 경우 캡처를 약한(weak) 참조로 정의하는 것이 낫죠. 약한 참조는 항상 옵셔널 유형이며, 참조하는 인스턴스가 할당 해제되면 자동으로 nil이 됩니다. 이를 통해 클로저 본문 내에서 해당 존재 여부를 확인할 수 있어요.

미소유 참조는 위의 Strong Reference Cycles for Closures 예제의 HTMLElement에서 강한 참조 순환을 해결하는 데 적합한 캡처 방법이에요. 순환을 피하기 위해 HTMLElement 클래스를 작성하는 방법은 다음과 같답니다:

class HTMLElement {
 
    let name: String
    let text: String?
 
    lazy var asHTML: () -> String = {
        [unowned self] in // self를 미소유 참조로 캡처하네요
        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")
    }
}
swift

HTMLElement 구현은 asHTML 클로저 내의 캡처 리스트 추가를 제외하고는 이전 구현과 동일해요. 이 경우 캡처 리스트는 [unowned self]인데, 이는 "self를 강한 참조가 아닌 미소유 참조로 캡처하라"는 의미랍니다.

이전과 같이 HTMLElement 인스턴스를 생성하고 출력할 수 있어요:

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

캡처 리스트가 있는 상태에서 참조는 다음과 같아요:

lecture image

이번에는 클로저에 의한 self의 캡처가 미소유 참조이며, 캡처한 HTMLElement 인스턴스를 강하게 유지하지 않아요. 아래 예제에서 볼 수 있듯이, paragraph 변수의 강한 참조를 nil로 설정하면 HTMLElement 인스턴스가 할당 해제되고 deinitializer 메시지가 출력된답니다:

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

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

요약하자면, 클로저와 클래스 인스턴스 간의 강한 참조 순환을 해결하려면 클로저 정의의 일부로 캡처 리스트를 사용해야 해요. 캡처 리스트에서 weak 또는 unowned 키워드를 사용하여 클로저에 의해 캡처된 참조를 약한 참조 또는 미소유 참조로 지정할 수 있죠. 이를 통해 불필요한 메모리 누수를 방지하고 앱의 성능과 안정성을 향상시킬 수 있답니다. 꼭 캡처 리스트를 활용해 보세요!