ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Apple Dev Reference - Swift] Strong Reference Cycles for Closures
    앱등이에게 살충제를 뿌린다./Apple Dev Reference 2016. 5. 15. 23:13

    서로 강한참조를 하면 절대 메모리에서 사라지지 않는다



    Strong Reference Cycles for Closures

    Strong Reference Cycles for Closures

    ARC & Strong Reference Cycle에서는 두 인스턴스가 서로 강하게 참조할 때 순환 참조(Strong Reference Cycle)가 발생하는 것을 살펴보았습니다. weak 참조와 unowned 참조를 사용하여 이를 해결하는 방법도 살펴보았습니다.


    클로져에서도 비슷한 경우가 있습니다. 클로져의 바디에 인스턴스의 프로퍼티나 인스턴스를 캡쳐하면 순환 참조가 발생할 수 있습니다. self.someProperty처럼 클로져에서 해당 인스턴스의 프로퍼티에 접근할 때나 self.someMethod()와 같이 인스턴스의 메소드를 호출할 때가 이에 해당합니다. 즉, 클로져에서 self를 캡쳐링 하면 순환 참조가 발생하게 됩니다.


    클로져 또한 클래스와 마찬가지로 참조타입이기 때문에 이러한 순환 참조가 발생하게 됩니다. 클로져를 프로퍼티에 대입할 때는 클로져의 주소 값이 대입되는 것입니다. 즉, 클로져에서 발생하는 순환 참조도 본질적으로는 두 인스턴스 간의 순환 참조와 같은 문제인 것입니다. 하지만 이 경우에는 두 클래스 인스턴스 간의 참조관계가 아닌 인스턴스와 클로져 간의 참조관게에서 발생한다는 차이점이 있습니다.


    Swift에서는 closure capture list를 사용하여 이 문제를 해결하고 있습니다. 순환 참조가 발생하는 과정을 살펴본 뒤에, 순환 참조를 해결하는 방법을 살펴보도록 하겠습니다.


    아래의 예제는 클로져에서 self를 사용했을 때 발생하는 순환 참조를 보여주고 있습니다. HTML문서의 각 태그에 대한 정보를 담는 HTMLElement클래스의 코드입니다.


    HTMLElement클래스는 h1pbr같은 html태그를 나타내는 name프로퍼티를 갖고 있습니다. 태그 사이에 들어갈 텍스트 값인 text프로퍼티도 갖고 있습니다.


    두 프로퍼티 외에 asHTML이라는 lazy 프로퍼티가 있습니다. asHTML은 name프로퍼티와 text프로퍼티를 결합해주는 () -> String 클로져 타입의  프로퍼티입니다.


    asHTML프로퍼티는 HTML tag를 리턴하는 클로져를 디폴트로 갖습니다. text의 값의 존재 여부에 따라 HTML태그 내부에 텍스트가 들어가게 됩니다. paragraph태그로 예를 들어보겠습니다. text에 값이 있다면 클로져는 "<p>some text</p>"를 리턴하고 없다면 "<p />"를 리턴하게 됩니다. 


    asHTML프로퍼티는 이름이나 사용법부터 메소드처럼 느껴지네요. 하지만 asHTML이 메소드가 아닌 클로져 타입의 프로퍼티이기 때문에 디폴트 클로져를 셋팅할 수도 있고, 개발자가 원하는 HTML폼 형태를 리턴해주는 커스텀 클로져를 셋팅할 수도 있습니다.(이건 그저 클로져의 특징중 하나이긴 하죠.)


    예를 들어 asHTML프로퍼티는 text프로퍼티가 nil일 때 특정 디폴트 텍스트를 삽입해주도록 셋팅해줄 수 있습니다. 이는 HTML태그에 빈 값이 들어가는걸 방지하기 위함입니다.


    Note

    asHTML은 lazy프로퍼티로 선언되어 있습니다. 디폴트클로져 내부에서 self 키워드를 사용할 수 있는 이유는 이 asHTML이 lazy프로퍼티이기 때문입니다. lazy프로퍼티는 인스턴스의 생성이 끝나고 self가 실제로 존재하기 전까지는 접근을 하지 않기 때문에 컴파일러가 감안해주는 것이죠~!


    HTMLElement클래스에는 name과 text를 파라미터로 받는 initializer가 있습니다. deinitializer도 제공하고 있습니다. deinitializer는 인스턴스가 해제될 때 메시지를 출력해주는 기능을 하고 있습니다.(예제에서 deinitializer는 인스턴스가 사라지는 시점을 알기 위해서 선언해준 것입니다.)


    아래는 HTMLElement클래스를 만들고 사용하는 예제입니다.


    Note

    위 예제에서 변수 paragraph는 옵셔널 HTMLElement타입입니다. 따라서 nil로 설정이 될 수 있습니다. 실제로 paragraph를 nil로 설정해보면 순환 참조가 발생한 탓에 인스턴스가 해제되지 않음을 확인할 수 있습니다.


    위에 말씀드렸듯, HTMLElement인스턴스와 인스턴스의 asHTML사이에 순환참조가 발생하였습니다. 아래 그림은 해당 참조관계를 보여주고 있습니다.


    인스턴스의 asHTML프로퍼티는 클로져 메모리공간(이건 인스턴스라 부를 수 없나요?)을 강하게 참조하고 있습니다. 하지만 클로져도 내부에서 self를 캡쳐하면서 self를 참조하고 있습니다.(self.nameself.text)  즉, 클로져 또한 self를 강하게 참조하고 있다는 뜻입니다. 인스턴스와 클로져 사이에 순환 참조가 발생한 것입니다. (클로져의 캡쳐링에 대해서는  Capturing Values.(TODO)을 참고해 주세요.)


    Note

    클로져에서 self를 여러번 호출하더라도 self에는 하나의 강한 참조만 발생합니다.


    변수 paragraph에 nil을 셋팅하여 변수-인스턴스 간 참조관계를 제거해도 인스턴스와 클로져는 해제되지 않습니다. 순환 참조 때문입니다.


    nil을 셋팅해도 HTMLElement의 deinitializer는 메시지를 프린트하지 않습니다. 인스턴스가 해제되지 않았기 때문이죠.




    Resolving Strong Reference Cycles for Closures

    인스턴스와 클로져 간의 순환 참조는 클로져를 선언할 때 캡쳐 리스트(capture list)를 선언함으로써 해결할 수 있습니다. 캡쳐 리스트란 클로져의 내부에서 사용할 레퍼런스 타입을 캡쳐링하는 룰을 나타내는 것입니다. 두 인스턴스의 순환 참조는 한 쪽을 weak나 unowned 참조로 선언함으로써 해결할 수 있었는데요, 클로져에서도 캡쳐할 레퍼런스를 weak나 unowned로 선언하면 순환 참조를 해결할 수 있습니다. weak 참조를 할 지, unowned 참조를 할 지는 또 개발자가 결정해야 주어야겠지요.


    Note

    Swift의 클로져 내부에서는 someProperty, someMethod()가 아닌 self.someProperty, self.someMethod()를 사용해야 합니다. 덕분에 여러분은 self가 캡쳐되는 경우(capture self by accident)를 미리 알 수 있습니다.



    Definiting a Capture List

    캡쳐 리스트에는 weak나 unowned 키워드와 인스턴스 레퍼런스(self)나 변수(delegate = self.delegate!)들을 같이 나열한 item들로 구성됩니다. 이 item은 대괄호안에 콤마로 구분하여 나열됩니다.

    클로져의 파라미터 선언 앞 부분에 적어주면 됩니다.


    만약 클로져가 파라미터나 리턴타입을 명시하지 않는다면(타입 추론 때문에 적지 않은 경우) 클로져의 시작 부분에 캡쳐 리스트를 적어주면 됩니다.





    Weak and Unowned References

    클로져와 클로져가 캡쳐한 인스턴스가 항상 서로를 참조하는 관계라면 인스턴스를 unowned 참조로 캡쳐를 해주세요. unowned 참조로 캡쳐를 해주면 클로져와 인스턴스는 항상 동시에 deallocated될 것입니다.


    반대로 캡쳐한 인스턴스가 nil이 될 가능성이 있는 경우라면 weak 참조로 캡쳐를 해주세요. weak 참조는 항상 옵셔널 타입입니다. 따라서 인스턴스가 deallocated가 되면 캡쳐한 값에는 nil이 들어가게 됩니다.


    Note

    만약 캡쳐하는 참조변수(인스턴스 또는 변수)가 절대로 nil이 되지 않는다면 unowned 참조로 캡쳐를 해주세요. nil이 될 가능성이 있다면 weak 참조로 캡쳐를 해주시구요.


    HTMLElement예제에서 순환 참조를 해결하기에는 unowned 참조로 캡쳐를 하는 것이 적절해 보입니다. 아래 예제는 순환 참조를 해결한 코드입니다.


    HTMLElement의 구현은 위와 동일합니다만 asHTML 클로져 프로퍼티에 캡쳐 리스트를 추가해주었습니다. [unowned self]로 캡쳐를 해주고 있습니다. 이 코드는 클로져 내부에서 self를 unowned 참조로 캡쳐하여 사용하겠다는 의미입니다.


    HTMLElement인스턴스를 생성하여 문장을 출력해보겠습니다.


    아래는 인스턴스와 클로져의 참조 관계입니다.


    클로져에서 self를 unowned 참조하기 때문에 HTMLElement인스턴스를 강하게 참조하지 않습니다. 강한 참조를 했던 위 예제에서는 paragraph 프로퍼티에 nil을 셋팅해도 인스턴스가 해제되지 않았습니다. 하지만 unowned 참조로 캡쳐한 이번 경우에는 인스턴스가 해제되고 deinitializer에서 메시지가 출력됨을 확인할 수 있습니다.



    출처 : https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html#//apple_ref/doc/uid/TP40014097-CH20-ID52

    참고 : ARC & Strong Reference Cycle

    참고 : Capture ListsCapturing Values

Designed by Tistory.