ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Apple Dev Reference - Swift] ARC & Strong Reference Cycle
    앱등이에게 살충제를 뿌린다./Apple Dev Reference 2016. 5. 15. 02:01



    Automatic Reference Counting

    Swift는 메모리관리를 위해 ARC(Automatic Reference Counting)를 사용하고 있습니다. 대부분 이 ARC덕분에 여러분은 메모리 관리를 신경쓸 필요가 없습니다. ARC는 필요없는 인스턴스를 자동으로 메모리에서 해제함으로써 메모리관리를 합니다.


    하지만 가끔은 ARC가 여러분의 코드로 메모리 관리를 할 때, 추가적인 정보가 필요할 때가 있습니다. 어떤 경우인지, 또 그런 상황은 어떻게 해결해야할 지 이 챕터에서  다뤄보겠습니다.


    Note : 참조 카운트는 클래스의 인스턴스에만 적용됩니다. Structure와 Enumeration은 Value타입이기 때문에 참조 카운팅이 필요 없습니다.




    How ARC Works

    새로운 인스턴스가 생성되면 ARC는 그 인스턴스를 위한 메모리 공간을 마련한 뒤 이 메모리에 인스턴스를 할당합니다. 이 메모리공간에는 인스턴스의 타입, 인스턴스에 있는 프로퍼티의 값들이 저장되어 있습니다.


    더이상 인스턴스가 필요하지 않다고 판단되면 ARC는 해당 인스턴스가 차지하고 있던 메모리를 해제해줍니다. 불필요한 인스턴스가 메모리 공간을 차지하는 경우를 방지하기 위함이죠.


    만약 ARC가 사용중인 인스턴스를 메모리에서 해제한다면 더 이상 이 인스턴스와 프로퍼티/메소드에 접근할 수 없게 될 것입니다. 이 상황에서 해당 인스턴스에 접근하려고 하면 아마도 앱이 크래쉬가 나겠죠.


    인스턴스가 더 이상 필요없어지는 시점을 알기 위해 ARC는 얼마나 많은 곳(프로퍼티, 변수, 상수 등)에서 인스턴스를 참조하고 있는지를 카운트합니다. 단 한 곳이라도 인스턴스를 참조하고 있다면 ARC는 이 인스턴스를 deallocate하지 않고 메모리상에 놔둡니다.


    여러분이 인스턴스를 생성하여 프로퍼티(또는 변수,상수)에 할당을 하면 프로퍼티는 이 인스턴스에 강한 참조(strong reference)를 발생시킵니다. "강한" 참조라고 이름을 붙인 이유는 해당 인스턴스를 꽉! 홀드하고 있기 때문입니다. 즉, 이 프로퍼티가 이 인스턴스를 참조하고 있는 한 절대로 ARC에 의한 deallocate가 되지 않을 것입니다.




    ARC in Action

    아래는 ARC의 작동에 대한 예제입니다. name이라는 프로퍼티를 갖는 Person클래스를 통해서 살펴봅시다.

    Person클래스는 name프로퍼티를 설정한 뒤 출력해주는 기능을 하는 초기화메소드를 갖고 있습니다. 또한 인스턴스가 deallocate될 때 메시지를 출력해주는 deinitializer도 갖고 있습니다.


    다음 자투리 코드에는 3개의 Person?타입 변수가 있습니다. 셋 다 하나의 인스턴스를 참조하게 될 변수들입니다. 이 세 변수들은 모두 옵셔널 타입이기 때문에 선언만 해주면 nil으로 초기화되어 어떤 인스턴스도 참조하지 않는 상태입니다.


    이제 새로운 Person타입 인스턴스를 생성하여 첫 번째 변수에 할당해줍니다.

    Person클래스의 생성자가 호출되었으므로 "John Appleseed is being initialized"이라는 메시지가 출력되었습니다. 이 메시지는 인스턴스 생성이 완료되었음을 알려줍니다.

    Person타입 인스턴스가 reference1변수에 할당되면서 이제 reference1과 Person인스턴스간에 강한 참조가 발생했습니다. 이제 한 개의 강한 참조가 있으므로 ARC는 Person인스턴스를 메모리상에 올려둡니다.


    아래처럼 인스턴스를 두 변수에도 할당해주면 두 개의 강한 참조가 추가로 발생합니다.


    이제 Person인스턴스에는 세 개의 강한 참조가 있습니다.

    세 개중 두 개의 변수에 nil을 대입(원래 변수인 reference1에도 nil)하여 강한 참조를 해제하면 한 개의 강한 참조만 남게 됩니다. 물론 Person인스턴스는 해제되지 않습니다.


    ARC는 마지막 강한 참조가 해제되기 전까지 Person인스턴스를 해제하지 않습니다. 아래처럼 마지막 강한 참조가 사라질 때 Person인스턴스는 해제되게 됩니다.




    Strong Reference Cycles Between Class Instances

    (위 소스를 참고하시고) ARC는 새로운 Person인스턴스가 생성되면 이 인스턴스에 대한 참조카운트를 기록합니다. 그리고 더 이상 인스턴스를 참조하는 곳이 없으면 인스턴스는 자동으로 메모리에서 deallocate되죠.


    하지만 인스턴스의 참조카운트가 절대로 0이 되지 않는 경우가 있다면 어떨까요? 두 인스턴스가 서로를 강하게 참조하고 있다면 이런 경우가 발생합니다. 두 인스턴스 모두 절대 참조 카운트가 0이되지 않습니다. 이런 경우를 순환 참조(reference cycle)라고 합니다.


    순환 참조를 해결하기 위해서는 인스턴스간 참조관계를 강한참조가 아닌 weak 또는 unowned참조로 바꿔주어야 합니다. 이 과정은 아래에 설명할 예정입니다. 하지만 그전에 이런 순환 참조가 발생하는 과정을 이해해보도록 하겠습니다.


    아래 코드는 강한참조가 발생하는 상황입니다. 이 예제에서는 Person클래스와 Apartment클래스를 선언하고 있습니다. 

    모든 Person인스턴스에는 String타입의 name프로퍼티와 옵셔널 apartment프로퍼티를 갖고 있습니다. 초기화할 때 apartment는 nil로 초기화 되겠군요. aprtment가 옵셔널인 이유는 모든 사람이 아파트를 소유하고 있지는 않기 때문입니다....ㅠ


    Apartment인스턴스도 유사한데요, String타입의 unit프로퍼티와 옵셔널타입의 tenant프로퍼티를 갖고 있습니다. 마찬가지로 초기화 시점에는 nil로 초기화 될거구요 이는 아파트에 주인이 있을수도 없을수도 있기 때문입니다.


    두 인스턴스 모두 deinitializer를 갖고 있습니다. 인스턴스가 해제될 때 메시지를 출력하는 기능을 갖고 있군요. Person인스턴스와 Apartment인스턴스가 해제되는 시점을 파악할 수 있기 위함입니다.


    아래 코드는 옵셔널Person 타입의 john, 옵셔널Apartment 타입의 unit4A. 두 변수를 선언하고 있습니다. 옵셔널이기 때문에 nil로 초기화가 되겠군요.

    이제 Person인스턴스와 Apartment인스턴스를 생성하여 두 변수에 할당해줍시다.


    이제 아래 그림과 같이 참조관계가 만들어졌습니다. john변수는 Person인스턴스를 강하게 참조하고 있고, unit4A변수는 Apartment인스턴스를 강하게 참조하고 있습니다.


    이제 두 인스턴스가 서로를 참조하는 상황을 만들어 봅시다. (무슨말이냐면 코드를 보면 이해가 되실겁니다!!)

    말그대로 어떤 사람이 아파트를 소유하게 되었고 그 아파트의 주인은 그 사람이 되는 상황입니다. john과 unit4A가 옵셔널이기 때문에 인스턴스에 접근하기 위해 느낌표(!)를 사용해주었습니다.

    이제 아래 그림처럼 참조관계가 만들어지겠죠?


    안타깝게도 이 두 인스턴스가 서로를 강하게 참조하면서 순환참조(Reference Cycle)가 발생했습니다. Person인스턴스는 Apartment인스턴스를 강하게 참조하고 있고, Apartment인스턴스 역시 Person인스턴스를 강하게 참조하고 있습니다. 따라서 john변수와 unit4A변수가 인스턴스에 대한 참조를 하지 않는 상황이 되어도 이 두 인스턴스의 참조 카운트는 0이 되지 않고 ARC는 인스턴스들을 메모리에서 해제하지 않습니다.

    두 변수가 nil로 설정되었음에도 불구하고 deinitializer는 호출되지 않았습니다. 이 순환 참조로 인해 Person인스턴스와 Apartment인스턴스는 메모리에서 해제되지 않고 메모리릭의 원인이 됩니다.


    아래는 john과 unit4A변수를 nil로 설정한 뒤의 참조관계입니다.


    Person인스턴스와 Apartment인스턴스가 서로를 참조하고 있는 저 화살표는 우리 힘으로 해제할 수 있는 방법이 없습니다.




    Resolving Strong Reference Cycles Between Class Instances

    Swift에는 순환참조를 해결할 두 가지 방법이 있습니다. 클래스 타입의 프로퍼티를 weak참조로 선언하거나 unowned참조로 선언하는 것인데요.


    weak참조와 unowned참조는 두 인스턴스가 상호참조를 할 때, 강한 참조를 하지 않도록 해줍니다. 즉, 순환 참조를 발생시키지 않고도 두 인스턴스가 서로를 참조할 수 있게 되는 것입니다. (*순환 참조는 서로가 참조한다는 뜻이 아닌 서로가 강하게 참조한다는 의미입니다. 헷갈리지 않으시길!)


    weak참조와, unowned참조는 어떻게 구분해서 사용할까요? 프로퍼티가 nil이 될 가능성이 있다면 weak참조로 선언해주세요. 반대로 프로퍼티가 절대로 nil이 되지 않는다는 보장이 된다면 unowned참조를 사용해 주세요. 




    Weak References

    weak reference는 해당 인스턴스에 대한 오너쉽을 가지지 않는 참조를 뜻합니다.(참조 카운트를 발생시키지 않는다는 뜻입니다.) 이 특징 덕분에 바로 순환 참조를 해결할 수 있게 되는 것입니다.(이유는 아래 설명됩니다.) 프로퍼티 및 변수를 선언할 때 왼쪽에 weak키워드를 사용하여 weak참조라고 선언할 수 있습니다.


    프로퍼티가 언제라도 nil이 될 가능성이 있다면 weak선언을 해주시고, 항상 값을 갖고있다는 것이 보장된다면 unowned선언을 해주세요. 

    위 Apartment예제에서는 apartment에 tenant가 없는 상황이 발생할 수 있습니다. nil이 될 가능성이 있다는 뜻이죠. 따라서 tenant를 weak로 선언해주면 순환 참조를 방지할 수 있을 것입니다.


    Note : weak참조는 런타임에서 값이 변할 수 있어야 하기 때문에 반드시 변수로 선언되어야 합니다. 상수로는 weak참조선언을 할 수 없습니다.


    weak참조는 인스턴스에 대한 강한 참조를 발생시키지 않기 때문에, 인스턴스에 대한 weak참조가 남아 있더라도 ARC는 인스턴스를 deallocate시킵니다. 인스턴스가 해제되면 자연스레 weak 참조에는 nil이 셋팅됩니다. weak참조는 언제든 nil이 될 가능성이 존재하므로 항상 옵셔널 타입으로 사용됩니다.


    아래는 위 코드의 Person클래스와 Apartment클래스의 예제와 딱 한 가지 차이점을 제외하고는 완전히 동일합니다. Apartment클래스의 tenant프로퍼티가 weak참조 타입으로 선언되었습니다.


    두 변수(john unit4A)의 선언과 인스턴스 간의 참조를 아래 코드를 통해서 진행하고 있습니다.


    이제 두 인스턴스와 변수간의 참조관계가 아래 그림처럼 만들어졌습니다.


    Person인스턴스는 여전히 Apartment인스턴스를 강하게 참조하고 있습니다.  하지만 Apartment인스턴스는 Person인스턴스를 weak하게 참조하고 있습니다. 이제 john에 nil을 셋팅하여 john과 Person인스턴스간의 강한 참조를 제거해주면 더 이상 Person인스턴스를 강하게 참조하고 있는 곳이 없어집니다. (위에서는 Apartment인스턴스가 여전히 강한참조를 하고 있었죠?)


    Person인스턴스에 대한 강한참조가 더 이상 존재하지 않기 때문에 tenant프로퍼티에는 nil이 셋팅됩니다.


    Note

    Garbage Collection을 사용하는 시스템에서 weak포인터는 인스턴스에 대한 강한 참조가 없어도 캐시역할을 하여 메모리에 여유가 있으면 해당 인스턴스를 해제하지 않고 남겨 둡니다. 하지만 ARC에서는 인스턴스에 대한 강한 참조가 없어지면 그 즉시 메모리에서 해제합니다.




    Unowned References

    weak 참조와 마찬가지로, unowned 참조(unowned reference) 또한 인스턴스에 대한 오너쉽을 갖지 않습니다. 하지만 weak 참조와 차이점이 있다면, unowned 참조는 항상 값을 갖고 있다는 것입니다. 때문에 unowned 참조타입 프로퍼티는 항상 nonoptional타입으로 사용됩니다. unowned 참조 프로퍼티 및 변수를 선언할 때 왼쪽에 unowned키워드를 적어주면 됩니다.


    unowned 참조는 nonoptional이기 때문에 값을 사용할 때 언래핑하는 과정이 없습니다. unowned 참조 타입 프로퍼티는 그냥 바로 사용하시면 됩니다. 그리고 ARC는 인스턴스가 해제되어도 프로퍼티에 nil을 대입하지 않는데요, 당연히 nonoptional 타입은 nil을 가질 수 없기 때문입니다.


    Note

    만약 참조중인 인스턴스가 해제된 unowned 참조 타입 프로퍼티에 접근하면 런타임 에러가 발생합니다. unowned 참조는 프로퍼티에 항상 인스턴스의 참조값이 있다고 보장될때만 사용하세요.(정말 지겹게 나오네요.) 


    Note also that Swift guarantees your app will crash if you try to access an unowned reference after the instance it references is deallocated. You will never encounter unexpected behavior in this situation. Your app will always crash reliably, although you should, of course, prevent it from doing so.


    아래 예제는 Customer와 CreditCard 클래스를 다루고 있습니다. 이 두 클래스는 서로 다른 클래스를 참조하기 위한 프로퍼티를 갖고 있습니다. 순환 참조의 발생 가능성이 있겠군요.


    Customer클래스와 CreditCard클래스는 위에서 weak참조 예제로 다룬 ApartmentPerson예제와 상황이 약간 다릅니다. 고객은 신용카드를 소유하고 있을수도, 소유하지 않을수도 있습니다. 하지만 신용카드의 입장에서 보면 항상 주인을 갖고 있어야 합니다. 이러한 상황을 표현하기 위해서 Customer클래스는 옵셔널타입으로 card프로퍼티를 갖고 CreditCard클래스에서는 nonoptional타입의 custmer프로퍼티를 갖습니다.


    신용카드는 항상 주인을 갖고 있어야 할 것이므로, customer프로퍼티는 unowned참조로 선언을 해주었습니다. 물론 순환 참조를 방지하기 위해서죠!


    Note

    CreditCard클래스의 number프로퍼티는 Int가 아닌 UInt64로 선언되어 있습니다. 32bit와 64bit시스템 모두에서 16자리 숫자를 잘 저장할 수 있게 하기 위함입니다.


    옵셔널 Customer타입인 john변수를 선언해줍니다. 옵셔널이기 때문에 nil로 초기화 될 것입니다.


    이제 새로운 Customer인스턴스를 생성하고 인스턴스의 card프로퍼티에 CreditCard인스턴스를 초기화하여 대입해줍시다.


    두 인스턴스의 참조관계는 아래 그림과 같이 만들어질 것입니다.


    Customer인스턴스는 CreditCard인스턴스를 강하게 참조하고 있고 CreditCard인스턴스는 Customer인스턴스를 unowned 참조하고 있습니다.


    customer가 unowned 참조이기 때문에 john변수-Customer인스턴스간의 강한 참조가 해제되면 더 이상 Customer인스턴스를 강하게 참조하는 곳이 없어지게 됩니다.


    Customer인스턴스에 대한 강한 참조가 존재하지 않기 때문에 ARC는 인스턴스를 해제합니다. 그렇게 되면 CreditCard인스턴스를 강하게 참조하는 곳도 더 이상 없습니다. 자연스레 CreditCard인스턴스도 해제됩니다.


    이 코드를 통해 john에 nil을 넣어주면 Customer인스턴스와 CreditCard인스턴스가 해제되었음을 확인할 수 있습니다.




    Unowned References and Implicitly Unwrapped Optional Properties

    위에서 weak 참조와 unowned 참조에 대한 예제를 통해 순환 참조가 발생할 수 있는 두 가지 상황에 대해서 살펴보았습니다.


    PersonApartment 예제에서 다룬 상호 참조 프로퍼티는 둘 다 nil이 될 수 있는 상황이었습니다. 이 경우 weak 참조를 사용하여 순환참조를 해결하는 것이 최선이었죠.


    CustomerCreditCard 예제에서는 한 쪽은 nil이 될 수 있지만 한 쪽은 절대 nil이 되지 않는 상황이었죠. 이 경우는 unowned 참조를 사용하여 순환참조를 해결했습니다.


    하지만, 세 번째 상황이 있습니다. 두 프로퍼티가 모두 위의 unowned참조 타입처럼 항상 값을 갖는 프로퍼티인 경우입니다. 이런 경우에는 한 쪽에서 unowned 참조 타입 대신 implicitly unwrapped optional을 사용하는 것이 좋습니다. 


    두 프로퍼티 모두 언래핑없이 직접 접근이 가능하고 순환 참조도 발생하지 않습니다. 이번 섹션에서는 이러한 참조관계를 만드는 법을 살펴보도록 하겠습니다. 


    아래의 예제는 Country클래스와 City클래스를 선언하고 있습니다. 각 클래스는 다른 클래스를 참조하는 프로퍼티를 갖고 있구요.(각 나라는 수도가 있고 수도 또한 나라를 갖고 있습니다.) Country클래스의 capitalCity프로퍼티와 City클래스의 country프로퍼티가 이에 해당합니다.


    두 클래스간의 상호의존성(Interdependency)을 만들어주기 위해 City클래스의 initializer는 Country인스턴스를 받아서 country프로퍼티에 대입해줍니다.


    City클래스의 initializer는 Country클래스의 initializer내부에서 호출되고 잇습니다. 하지만 Country클래스의 initializer는 City의 initializer에 self를 전달할 수 없습니다. 왜냐하면 아직 initializer메소드가 끝나지 않아 인스턴스가 완전히 생성되지 않았기 때문입니다. (해당 내용 참조 :  Two-Phase Initialization)


    이 문제를 해결하기 위해 Country클래스의 capitalCity프로퍼티를 implicitly unwrapped optional 프로퍼티로 선언해 줍니다. (City! 이렇게 타입의 오른쪽에 느낌표를 찍어줌으로써 선언합니다.) 이는 capitalCity프로퍼티는 일반 옵셔널처럼 디폴트로 nil값을 갖지만 언래핑 없이 접근할 수 있는 옵셔널이라는 의미입니다.

    Implicitly Unwrapped Optiaonals(원문) :  Implicitly Unwrapped Optionals.

    Implicitly Unwrapped Optiaonals(번역) :  Implicitly Unwrapped Optionals.


    capitalCity프로퍼티는 디폴트로 nil값을 갖기 때문에 Country인스턴스의 초기화는 name프로퍼티가 세팅되면 끝나게 됩니다. 그 결과 Country 클래스의 initializer내부에서 name프로퍼티가 설정된 뒤에는 self키워드를 사용할 수 있게 됩니다. Country클래스의 initializer가 City클래스의 initializer에 self를 넘길 수 있는 방법에는 이런 비밀이 숨겨져 있었던 겁니다!!


    이 덕분에 Country인스턴스와 City인스턴스를 한 문장으로 순환참조 없이 생성했습니다. 


    implicitly unwrapped optional을 사용하여 two-phase class initializer의 요구사항을 만족시켰습니다. capitalCity프로퍼티는 강한 참조를 발생시키지 않으며 nonoptional 타입처럼 사용할 수 있습니다.



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

    관련 문서 : Strong Reference Cycle for Closures (클로져 사용 시 발생할 수 있는 순환참조)

    관련 문서 : Block 사용 시 발생할 수 있는 순환참조

Designed by Tistory.