(Swift) 클로저 내부에서 self를 캡쳐하는 방법

항상 [unowned self]를 써야 할까요?

http://stackoverflow.com/questions/24320347/shall-we-always-use-unowned-self-inside-closure-in-swift 의 내용중 일부를 번역하였습니다.

아니오, 분명히 [unowned self]를 쓰지 말아야할 상황이 존재합니다. 특정한 클로져가 호출되었을 때에
그 시점까지 self가 파괴되지 않고 살아있음을 보장할 필요가 있을 때가 있단 말이죠.

예를 한가지 들어봅시다. 비동기 네트워크 요청을 하고, 요청에 대한 응답을 받았을 때 self로부터
어떤 무언가를 호출해야 한다고 가정해봅시다. 비동기 요청이 끝나는 시점 이전에 해당 객체가 해제되어 사라졌다면
프로그램이 뻗고 말겁니다.

그렇다면 언제 unowned selfweak self를 써야하는가? 바로 강한 참조 사이클이 만들어질 때입니다.
이는 두개 이상의 객체가 서로 순환하면서 서로를 순서대로 참조해나갈 때 생길 수 있습니다. 이 경우 사이클 내의 객체들은 어떤 경우에도 항상 참조수 1 이상을 갖기 때문에 파괴되지 않고 리소스를 낭비하게 됩니다.

클로저의 경우, 클로저가 캡쳐한 – 즉 클로저 내에서 선언되지 않았지만, 사용은되는- 모든 객체는 클로저에 의해 ‘소유’됩니다. 따라서 클로저가 남아있다면, 해당 객체는 제거되지 않습니다. 이 순환고리르 끊는 방법 역시 [unowned self][weak self]를 쓰는 것입니다. 만약 어떤 클래스가 클로저를 소유하고, 클로저가 또 해당 클래스에 대해 강한 참조를 가지면 클래스와 클로저 사이에도 강한 참조 사이클이 발생하게 됩니다. 혹은 클로저가 다른 객체를 소유하고, 해당 객체가 다시 클로저를 소유하고, 클로저는 다시 해당 객체를 소유하는 3각 이상의 관계에서도 적용될 수 있습니다.

이런 예들에 대해서 애플 공식 문서는 아주 훌륭한 예를 들어서 설명하고 있습니다.

https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html

먼저 아래의 예를 보겠습니다. 아래 HTML 클래스는 asHTML이라는 메소드 를 가지고 있습니다. 메소드는 클로저의 일종이기는 합니다만, 메소드 인스턴스 객체는 참조 순환을 만들지 않습니다. 왜냐하면 메소드가 호출되는 시점에는 항상 객체 인스턴스가 유효해야 하므로 굳이 캡쳐링할 필요가 없기 때문이죠.

class HTML {
    let tagName: String
    let defaultContent: String = "hello, world"
    var content: String?

    init(tagName: String) {
        self.tagName = tagName
    }

    func asHTML() -> String {
        return "<\(self.tagName)> \(self.content ?? self.defaultContent) </\(self.tagName)>"
    }

    deinit {
        print("\(tagName) is deallocated...")
    }
}

var h: HTML? = HTML(tagName:"h1")
print(h?.asHTML())
h = nil

따라서 아래와 같이 실행하면 해당 객체에 대한 참조가 사라지는 시점에 객체가 해당된다는 메시지가 출력됩니다.

var h: HTML? = HTML(tagName: "H1")
print(h!.asHTML())
h = nil

/// <h1> hello, world </h1>
/// h1 is deallocated...

하지만 아래와 같이 이를 클로저로 바꾼다면 어떨까요?

class HTML {
    let tagName: String
    let defaultContent: String = "hello, world"
    var content: String?

    init(tagName: String) {
        self.tagName = tagName
    }

    lazy var asHTML: () -> String = { 
        return "<\(self.tagName)> \(self.content ?? self.defaultContent) </\(self.tagName)>"
    }

    deinit {
        print("\(tagName) is deallocated...")
    }
}

var h: HTML? = HTML(tagName:"h1")
print(h!.asHTML())
h = nil

이 때는 해당 객체가 자동으로 제거되지 않습니다.

여기서 잠깐, 왜 lazy한 프로퍼티로 설정했냐 하면, lazy하지 않은 프로퍼티로 만드는 경우에 해당 객체의 초기화가 끝나지 않은 시점에 self를 참조하는 셈이 되므로 이는 초기화 룰을 위반하기 때문입니다.1

아무튼 이 경우에는 자체적으로 객체와 클로저(어쨌든 이 클로저는 호출되려는 시점에 생성됩니다.)가 서로 순환 참조를 가지게 되므로, 다음과 같이 변경해야 합니다.

lazy var asHTML: () -> String = { [unowned self] in 
  return "<\(self.tagName)> \(self.content ?? self.defaultContent) </\(self.tagName)>"
}

  1. lazy하지 않은 모든 저장 프로퍼티는 초기화가 끝나기 전까지 초기값을 가져야하며, 초기화 중에는 self를 쓰는 것이 위험합니다. 특히 init()외부에서 초기화되는 경우에 이는 초기화 과정 이전에 실행되는 메소드이므로, [unowned self] in은 반드시 필요합니다. 
  • Daeung Kim

    정말 도움이 되었습니다.