ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • ARC(Autometic Reference Counting)
    Swift 2020. 2. 3. 17:38
    - ARC(Autometic Reference Counting): Reference 개수를 세면서 자동으로 메모리를 관리해주는 방식입니다.
    - 참조하는 Reference 개수가 0이 되면 자동으로 힙 메모리에서 제거합니다.
    - 만약 서로 강하게 참조하고 있다면 reference 개수가 0이 될 수 없어 메모리 누수가 발생합니다.
    - 이를 방지하기 위한 방법으로 weak, unowned 가 있습니다. weak와 unowned는 reference count를 증가시키지 않고 참조하는 방식입니다.
    - 이미 메모리에 존재하지 않는 대상에 접근하려고 하면 런타임 에러가 발생합니다. 따라서 reference count를 증가시키지 않는 만큼 메모리에서 해제된 대상에 접근하지 않도록 주의해야 합니다.
    - weak는 참조하고 있는 대상이 메모리에서 해제되면 자동으로 nil을 할당해줍니다. 따라서 weak의 대상은 반드시 optional 타입이어야 합니다.
    - 하지만 optional이면 안되는 경우도 있습니다. 이때는 unowned를 사용합니다. 따라서 참조하는 대상보다 같거나 더 긴 라이프 사이클에 사용해야 합니다.

     

    ARC란 자동으로 Reference 개수를 관리해주는 것을 의미합니다. 
    Swift에서는 ARC를 사용해 개발자의 메모리 관리에 대한 부담을 덜어줍니다.
    좋고 편리한 것은 맞지만 절대적인 것은 아닙니다. 동작 과정과 원리를 이해하지 못하고 ARC만 무조건 믿고 있다가는
    나도 모르는 사이에 앱 이곳 저곳에서 메모리 누수와 각종 문제가 발생할 수 있습니다. 

    ARC가 무엇인지 어떻게 작동하는지 알아보고
    어떤 경우에 문제가 발생할 수 있는지
    해결책은 무엇인지 
    같이 살펴보도록 하겠습니다.

    공식 문서를 참고해서 나름대로 정리한 글입니다. :)

     


     

    들어가기전에 기본적으로 잡고가야 할 개념은 ARC라는 이름에서도 알 수 있듯이 reference를 관리한다는 점입니다.
    즉, reference를 사용하는 class 인스턴스에 한해서만 적용되는 개념입니다. struct나 enum은 value type 으로 해당되지 않습니다. 
    이 부분에 대해 조금 더 자세한 내용이 궁금하시면 이 글을 먼저 읽고 오시면 도움이 되실 것 같습니다.

    How ARC Works

    class instance가 하나 생성될 때마다 ARC는 해당 인스턴스의 정보를 저장하기 위해 메모리를 할당합니다.
    참조 중인 reference를 해제해버린다면 해제된 reference에 접근하려고 하는 순간 크래시가 발생하겠죠?
    따라서 reference를 관리하기 위해 참조 카운트를 하나 늘리고 이 참조 카운트가 0이 되면 참조를 해제합니다.

    어렵지 않죠? 아주 간단한 원리입니다. 인스턴스가 어떻게 참조되는지 보고있다가 참조되는게 없으면 해제!

    하지만 실제로 그렇게 간단하지만은 않습니다.
    구체적인 예를 살펴보면서 확인해볼까요?

    ARC in Action

    먼저 하나의 인스턴스를 여러 곳에서 참조하고 있을 경우,
    모두 해제해주지 않으면 실제로는 해제되지 않습니다.

    다음과 같이 Person class를 정의해보겠습니다.

    class Person {
        let name: String
        init(name: String) {
            self.name = name
            print("\(name) is being initialized")
        }
        deinit {
            print("\(name) is being deinitialized")
        }
    }
    
    var reference1: Person?
    var reference2: Person?
    var reference3: Person?

    Person 타입의 optional 변수 3개를 생성했으나 아직 참조되지 않은 상태입니다. 

    여기에 다음과 같이 인스턴스를 할당해보면 어떻게 될까요?

    reference1 = Person(name: "John Appleseed")
    // Prints "John Appleseed is being initialized"
    
    reference2 = reference1
    reference3 = reference1

    reference1에서 Person 인스턴스가 처음 할당되고 이 인스턴스를 reference2, reference3 에서 참조함으로써 Person 인스턴스에 3개의 참조가 있는 상태입니다. 이 상태에서는 다음과 같이 2개의 인스턴스를 제거해도 실제로는 참조가 해제되지 않습니다.

    reference1 = nil
    reference2 = nil

    참조하고 있는 3개의 인스턴스를 모두 제거해야만 참조가 해제됩니다.

    reference3 = nil
    // Prints "John Appleseed is being deinitialized"

     

    Strong Reference Cycles Between Class Instances

    다음으로 인스턴스가 서로를 강하게 참조하고 있다면 제대로 참조가 해제되지 않을 수 있습니다.

    "강하게" 라는 의미는 조금 후에 살펴보겠습니다.

    앞서 본 Person class와 Apartment class를 예로 들어보겠습니다.

    class Person {
        let name: String
        init(name: String) { self.name = name }
        var apartment: Apartment?
        deinit { print("\(name) is being deinitialized") }
    }
    
    class Apartment {
        let unit: String
        init(unit: String) { self.unit = unit }
        var tenant: Person?
        deinit { print("Apartment \(unit) is being deinitialized") }
    }
    
    var john: Person?
    var unit4A: Apartment?

    john은 아파트를 가질 수 있고 unit4A라는 아파트 역시 john이라는 세입자를 가질 수 있습니다.

    john = Person(name: "John Appleseed")
    unit4A = Apartment(unit: "4A")

    이때 john과 unit4A가 서로를 강하게 참조하고 있다면 어떻게 될까요??

    john!.apartment = unit4A
    unit4A!.tenant = john

    이 경우 치명적인 문제가 발생할 수 있습니다.
    john 을 제거하면 어떻게 될까요? tenant 변수에서 강하게 참조하고 있기 때문에 Person instnace는 해제될 수 없습니다.
    반대로 unit4A를 제거해도 apartment 변수에서 강하게 참조하고 있기 때문에 Apartment instance는 해제될 수 없습니다.

    Resolving Strong Reference Cycles Between Class Instances

    이 문제를 해결하려면 어떻게 해야할까요? 앞서 "강하게" (strong) 라는 표현을 사용했는데요, 이런 경우를 피하기 위해 도입된 개념이 weak reference 또는 unowned reference 입니다.

    Weak References

    weak reference를 사용하면 참조하고 있는 상태라 하더라도 참조하는 인스턴스가 해제되었을 때 ARC가 자동으로 nil값을 할당해줍니다.
    따라서 weak 변수는 nil이 될 수 있는 optional 타입이어야 합니다. 

    Property observer는 weak reference가 nil이 되어도 호출되지 않습니다. 
    class Person {
        let name: String
        init(name: String) { self.name = name }
        var apartment: Apartment?
        deinit { print("\(name) is being deinitialized") }
    }
    
    class Apartment {
        let unit: String
        init(unit: String) { self.unit = unit }
        weak var tenant: Person?
        deinit { print("Apartment \(unit) is being deinitialized") }
    }
    
    var john: Person?
    var unit4A: Apartment?
    
    john = Person(name: "John Appleseed")
    unit4A = Apartment(unit: "4A")
    
    john!.apartment = unit4A
    unit4A!.tenant = john

    앞의 예에서 tenant 변수를 weak로 참조하고 있다면, john을 제거했을 때 정상적으로 deinit되고 tenant는 nil이 됩니다.

    john = nil
    // Prints "John Appleseed is being deinitialized"

    이후 unit4A를 제거하면 unit4A도 정상적으로 제거됩니다.

    garbage collection은 메모리 압박이 있을 경우에만 실행되기 때문에 
    garbage collection을 사용하는 시스템에선, weak pointers가 캐싱 매커니즘(caching mechanism)을 구현하는데 사용됩니다. 
    하지만 ARC 에서는 변수가 강한 참조가 해제되자마자 변수가 deallocated되기 때문에 캐싱 매커니즘에는 적합하지 않습니다.

    Unowned References

    unowned reference도 weak와 비슷하게 강한 참조를 하지 않습니다.
    차이점은 unowned reference는 그 자체가 값을 갖습니다.
    weak는 참조 대상이 제거되었을 때 ARC가 자동으로 nil값을 주지만, unowned는 nil값을 주지 않습니다.
    따라서 unowned는 non-optional 타입을 사용
    하고 해당 인스턴스와 같거나 혹은 더 긴 lifetime을 가질 때 사용됩니다.

    unowned reference는 참조할 인스턴스가 해제되지 않는다고 확신할 수 있을 때 사용해야 합니다.
    unowned로 참조된 인스턴스가 해제되었는데 접근하려고 하면 runtime error가 발생합니다.

    예를 들어보겠습니다.

    class Customer {
        let name: String
        var card: CreditCard?
        init(name: String) {
            self.name = name
        }
        deinit { print("\(name) is being deinitialized") }
    }
    
    class CreditCard {
        let number: UInt64
        unowned let customer: Customer
        init(number: UInt64, customer: Customer) {
            self.number = number
            self.customer = customer
        }
        deinit { print("Card #\(number) is being deinitialized") }
    }
    
    var john: Customer?
    
    john = Customer(name: "John Appleseed")
    john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
    16자리 카드 넘버를 32비트, 64비트에 관계없이 수용하기 위해 Int대신 UInt64를 사용했습니다.

    이 경우 Customer는 Creditcard를 강하게 참조하고
    CreditCard의 customer는 nil이 될 수 없는 인스턴스 입니다. 오직 개인의 소유기 때문이죠.

    CreditCard의 customer는 nil이 될 수 없기 때문에 unowend를 사용하지 않은 상태에서 john을 제거해버리면 둘다 해제되지 않은 상태가 되어버립니다. unowned를 사용하면 강하게 참조하는 john이 제거될 때 CreditCard는 더 이상 참조되지 않기 때문에 안전하게 제거됩니다.

    즉, unowned 참조는 자신을 참조하는 대상이 사라지면 제거됩니다.
    위 예에서는 safe unowned reference를 사용합니다. Swift는 퍼포먼스를 위해 runtime safety check를 막아야 할 필요가 있는 경우를 위해 unsafe unowned reference도 제공합니다. 이때는 사용자가 안전성 체크를 책임져야 합니다.

    만약 참조된 인스턴스가 할당 해제된 이후에 unowned(unsafe) 레퍼런스에 접근하려고 하면 프로그램이 해당 인스턴스가 사용된 메모리 지역에 접근합니다. 안전하지 않은 동작입니다.

    ...
    어렵네요... 위 예시에서 보면 john이 해제된 이후에 john.card에 접근하는 경우를 말하는 것 같은데.. 제대로 이해를 한건지 모르겠습니다 ㅠ-ㅠ
    위 예에서 unowned(unsafe)를 사용해도 john = nil로 할당해제 한 뒤 john?.card 카드에 접근하거나 값을 넣으려 해도 따로 차이가 보이진 않는데 말이죠.. 메모리적으로는 다르게 동작하는걸까요? 아니면 애초에 잘못 이해하고 있는걸까요... 추후에 알게되면 다시 포스팅해야겠습니다.. 
    혹시 아시는 분이 계시면 댓글로 알려주시면 감사하겠습니다 :(

     

    Unowned References and Implicitly Unwrapped Optional Properties

    위의 예제들은 강한 순환 참조(strong reference cycle)를 깨야하는 하는 경우들을 보여줍니다. 

    Person과 Apartment 예제는 두 프로퍼티가 nil 값을 받아도 서로 참조하고 있어 해제되지 않았던 상황입니다. 
    weak로 해결할 수 있었습니다.

    Customer와 CreditCard 예제는 한 프로퍼티는 nil값을 받아도 나머지는 nil을 받을 수 없는 경우 유발될 수 있는 문제였습니다.
    CreditCard는 한 사람을 위한 것이기 때문에 CreditCard의 customer는 nil 이면 안되는 non-optional 타입인데,
    서로를 강하게 참조하고 있기 때문에 정상적으로 해제되지 않는 경우였습니다.
    이때 unowned로 해결할 수 있었습니다.

    하지만 이외에 세번째 경우도 있습니다. 두 프로퍼티 모두 값을 갖고있고 한 번 생성되면 절대로 nil이 될 수 없는 경우입니다.
    이 경우 unowned property를 갖는 한 클래스와 강제 언레핑(!)된 옵셔널 프로퍼티를 합치면 매우 유용합니다.

    이 방법을 사용하면 순환 참조를 피하면서 한번 초기화되면 서로 직접 접근할 수 있습니다.

    class Country {
        let name: String
        var capitalCity: City!  // init에서 초기화되기 때문에 안전하게 ! 사용
        init(name: String, capitalName: String) {
            self.name = name
            self.capitalCity = City(name: capitalName, country: self)
        }
    }
    
    class City {
        let name: String
        unowned let country: Country  // unowned 사용
        init(name: String, country: Country) {
            self.name = name
            self.country = country
        }
    }

    두 클래스 사이에 상호 의존성을 갖게 하기위해
    City의 초기화에서 country 객체를 받아 country 프로퍼티에 저장합니다.

    City의 초기화는 Country 초기화 안에서 이루어집니다. 따라서 Country가 초기화 되기 전까지는 self를 전달할 수 없습니다. 
    이러한 초기화 방식을 Two-Phase Initialization 라고 합니다.

    var country = Country(name: "Canada", capitalName: "Ottawa")
    print("\(country.name)'s capital city is called \(country.capitalCity.name)")
    // Prints "Canada's capital city is called Ottawa"

    이처럼 Country 객체를 생성하면 내부의 capitalCity가 안전하게 생성되어 접근이 가능해집니다.

    Strong Reference Cycles for Closures

    closure 에 class instance를 할당하면 클로저 내부에서 instance를 캡쳐하고 있기 때문에 순환 참조가 발생할 수 있습니다.
    클로저 내부에서 self.someProperty, self.someMethod() 를 사용할 때 클로저 내부에서 self를 "캡쳐"하고 있기 때문에 순환참조를 발생시킵니다.

    이는 closure도 class와 같이 reference type이기 때문에 발생합니다. 하나의 클로저를 프로퍼티에 적용하면 그 클로저에 하나의 래퍼런스를 만드는 것입니다. 이는 위에서 봤던 두 래퍼런스가 서로 강하게 참조하는 것과 본질적으로 같은 문제입니다. 클래스 인스턴스와 클로저가 살아남는 차이일 뿐입니다. 

    Swift는 closure capture list 라는 멋진 방법을 제공합니다. 해결책을 알아보기전에 어떻게 문제가 발생하는지 이해해보면 좋겠죠?

    아래 예는 closure가 self 래퍼런스를 참조하는 경우 어떻게 문제가 발생하는지 보여줍니다.

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

     

     

    간단하게 tag name과 text를 갖는 HTMLElement 클래스 입니다. 

    asHTML은 text 값이 있냐 없냐에 따라 각각 처리됩니다. 
    하지만 asHTML가 인스턴스 메서드가 아닌 클로저기 때문에 asHTML의 기본 값을 바꿀 수 있습니다.
    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>"
    asHTML은 lazy로 선언되었는데 출력 대상에 대한 값으로 렌더링 되는 경우에만 필요하기 때문이다.
    실제로 lazy는 초기화가 완료되어 self를 알 수 있어야 접근할 수 있기 때문에 클로저 내부에서 self를 사용할 수 있다.

    ...
    lazy에 대해서도 포스팅해야겠습니다.

    생성자를 통해 HTMLElement를 만들면 강한 순환참조를 만듭니다.

    asHTML 프로퍼티가 클로저에 강한 참조를 갖습니다. 그리고 클로저 안에서 다시 self를 참조하기 때문에 서로 강한 순환 참조를 만듭니다.

    클로저 안에서 self를 여러번 참조해도 하나의 강한 참조만을 갖습니다.

    만약 여기서 paragraph 프로퍼티를 제거해도 프로퍼티와 클로저는 할당 해제되지 않습니다.

    Resolving Strong Reference Cycles for Closures

    capture list 를 정의함로써 이 문제를 해결할 수 있습니다. 클로저 내부에서 하나 이상의 래퍼런스를 참조할 때 capture list를 정의합니다. weak를 선택할 지 unowned를 선택할 지는 다른 부분과의 관계에 따라 달라집니다.

    Swift는 클로저 내부에서 self 멤버를 참조하려고 할 때 그냥 someProperty 나 someMethod()가 아니라 self.someProperty나 self.someMethod() 를 사용하도록 강제합니다. 이를 통해 self가 캡쳐될 수 있음을 기억하도록 돕습니다.

    Defining a Capture List

    capture list는 [ ] 안에서 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

    클로저와 캡쳐하는 인스턴스가 항상 서로를 가리키고 동시에 할당해제 된다면 unowned를 사용해 정의합니다.

    반대로 캡쳐된 래퍼런스가 nil이 될 수 있을 때 weak를 사용해 정의합니다.
    weak reference는 항상 옵셔널 타입이며 인스턴스가 할당 해제되면 자동으로 nil이 됩니다. 이를 통해 클로저 내부에서 인스턴스의 존재를 확인할 수 있습니다.

    만약 절대 nil이 될 수 없는 구조라면 unowned로 정의되어야 합니다.

    위의 예에서는 unowned reference를 사용하는게 적절합니다.

    class HTMLElement {
    
        let name: String
        let text: String?
    
        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")
        }
    
    }

    HTMLElemenet의 구현은 캡쳐리스트의 추가와는 관계없이 이전과 변함이 없습니다. 하지만 이 경우 항상 서로를 가리키면서 라이프사이클을 함께 해야하죠. 이럴 때 unowned를 사용해야 합니다.

     

     

     

    참고

    https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html#//apple_ref/doc/uid/TP40014097-CH20-ID48

     

    Automatic Reference Counting — The Swift Programming Language (Swift 5.1)

    Automatic Reference Counting Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, this means that memory management “just works” in Swift, and you do not need to think about memory management yourself. A

    docs.swift.org

     

    https://nealbaek.tistory.com/entry/ARC-Swift-3-Swift-ARC-App-doc-%EB%B2%88%EC%97%AD#recentEntries

     

    [ ARC , Swift 3 ] Swift ARC, App doc ( 번역 )

    Swift ARC, App doc ( 번역 ) 원본입니다! https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html#//apple_ref/doc/uid/TP40..

    nealbaek.tistory.com

     

    'Swift' 카테고리의 다른 글

    associatedType  (2) 2020.03.18
    @discardableResult  (0) 2020.03.18
    escaping closure  (0) 2020.01.30
    mutating  (0) 2019.11.19
    inout  (0) 2019.10.24

    댓글

Designed by Tistory.