본문 바로가기
Swift 공식 가이드/Swift 3

Automatic Reference Counting (ARC)

by 토끼찌짐 2017. 3. 14.

Apple 제공 Swift 프로그래밍 가이드(3.0.1)의 Automatic Reference Counting 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.



들어가며

Swift는 Automatic Reference Counting (ARC) 을 통해 앱의 메모리 사용량을 추적하고 관리한다. ARC는 클래스의 인스턴스가 더이상 필요하지 않다고 판단하면 자동으로 해당 인스턴스의 메모리를 해제해준다. 대부분의 경우 잘 동작하기 때문에 그냥 맡겨놔도 되지만, 그럼에도 개발자가 신경써야 할 몇 가지 케이스가 있다. 이번 챕터에서는 그런 케이스에 대해서 알아보고, ARC가 앱 메모리 관리를 어떻게 하고 있는지도 알아보자.

<note> 레퍼런스 카운팅은 오직 클래스의 인스턴스에만 적용된다. 구조체와 ENUM은 값 타입이기 때문에 참조reference가 저장되거나 전달되는 일이 없다.



How ARC Works

클래스의 인스턴스가 만들어질 때 ARC는 메모리를 할당해준다. 할당된 메모리가 가지hold 정보는 인스턴스의 타입 정보, 인스턴스와 연관된 stored 프로퍼티의 값들이다.

클래스의 인스턴스가 더 이상 필요하지 않을 때 ARC는 할당된 메모리를 해제하여 다른 목적으로 쓰일 수 있도록 해준다. (쓸데없는 메모리 차지를 방지)

ARC는 아직 사용중인 인스턴스를 해제하지 않도록 주의를 기울인다. 만약 사용중인 인스턴스가 해제deallocate될 경우 인스턴스의 프로퍼티나 메서드에는 더 이상 접근할 수 없게 되기 때문에 Crash의 위험이 있기 때문이다.

따라서 ARC는 클래스 인스턴스를 참조하고 있는 프로퍼티, 상수, 변수들을 추적하고, 만약 "strong" 참조가 하나라도 있다면 해제하지 않는다.
(클래스 인스턴스가 프로퍼티, 상수, 변수 등에 할당될 때는 strong 참조가 되는데, 이게 하나라도 걸려있다면 ARC는 해당 인스턴스가 사용중이라고 판단하여 해제를 하지 않는다는 말이다.)



ARC 실제 동작 살펴보기

ARC가 실제로 어떻게 동작하고 있는지를 살펴보자. 예시를 위해 name이라는 상수 프로퍼티를 가진 Person이라는 클래스를 준비했다.

class Person {
    let name: String
    
    init(name: String) {
        self.name = name
        print("\(name) init")
    }
    
    deinit {
        print("\(name) deinit")
    }
}

var reference1: Person?
var reference2: Person?
var reference3: Person?

reference1 = Person(name: "찜토끼") // print "찜토끼 init"
// 변수에 클래스의 인스턴스가 할당될 때는 strong 참조
// reference1과 Person의 인스턴스(찜토끼 인스턴스) 사이에 strong 참조가 생김

reference2 = reference1
reference3 = reference1
// 찜토끼 인스턴스에 대한 strong 참조는 3개가 됨

reference1 = nil
reference2 = nil
// 찜토끼 인스턴스에 대한 strong 참조는 1개가 됨
// ARC는 아직 이 인스턴스를 deallocate 하지 않음

reference3 = nil // print "찜토끼 deinit"
// 찜토끼 인스턴스는 이제 어떤 strong 참조도 걸려있지 않아서 dealloc



클래스 인스턴스 사이의 Strong Reference Cycle

클래스의 인스턴스를 strong 참조하고 있는 수가 0이 되면 ARC가 해제를 해준다는 것을 알아보았다.

그런데 어떤 경우에는 strong 참조수가 끝까지 0으로 떨어지지 않아서 끝까지 해제가 되지 않기도 한다. 두 클래스 인스턴스가 서로를 strong 참조로 잡고 있는 경우가 그러하다.

서로가 서로를 계속 살아있게 만드는 이런 경우를 Strong Reference Cycle이라고 하며 이것은 메모리 누수로 이어지기 때문에 주의해야 한다. 해결법은 weak 참조나 unowned 참조다.

해결법을 알아보기 전에 우선 왜 이런 현상이 발생하는지를 예제로 살펴보자. 아파트를 위한 Apartment 클래스와 아파트의 주민을 위한 Person 클래스를 준비하였다.

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 = Person(name: "john Appleseed")
// john과 인스턴스 사이는 strong 참조

unit4A = Apartment(unit: "4A")
// 마찬가지로 strong 참조

john!.apartment = unit4A // strong 참조
unit4A!.tenant = john // strong 참조

john = nil
unit4A = nil
// 이제 john과 <Person instance> 사이의 strong 참조는 없고
// unit4A와 <Apartment instance> 사이의 strong 참조도 없다
// 그러나 deinit은 불리지 않는다 (어느 인스턴스도 dealloc되지않음)
// 그 이유는 아래 일러스트를 보면 된다

이렇듯 인스턴스 두 개가 서로에 대해 strong 참조가 걸려있기 때문이다. (ARC는 strong 참조가 하나라도 걸려 있으면 메모리를 해제하지 않음)



<해결하기> 클래스 인스턴스 사이의 Strong Reference Cycle

클래스 타입의 프로퍼티를 사용하며 strong reference cycle을 피할 수 있는 방법은 두 가지가 있다. weak 참조와 unowned 참조이다. 이것들은 reference cycle 에 속한 한 인스턴스에게 "다른 인스턴스를 꽉 잡고strong hold 있지 않아도 된다"고 알려주고, 따라서 strong reference cycle을 피할 수 있다.

  • weak 참조 : Use a weak reference when the other instance has a shorter lifetime—that is, when the other instance can be deallocated first (내가 살아있는 동안, 내가 참조하는 대상이 nil이 될 수도 있을 때 사용)

  • unowned  참조 : Unowned reference is used when the other instance has the same lifetime or a longer lifetime (내가 살아있는 동안, 내가 참조하는 대상이 nil이 될 일이 없을 때 사용)


<weak 참조>

  • 이름 그대로 약한 참조

  • 프로퍼티나 변수 선언할 때 weak 키워드를 앞에 붙이면 weak  참조

  • 참조대상이 어느 순간 nil이 될 수 있을 때 사용한다

  • 아직 참조가 되고 있다고 하더라도 strong 참조가 아니기 때문에 ARC에 의해 해제될 가능성이 있다. 이 경우 ARC는 weak 참조에 nil을 할당해준다. (따라서 nil체크를 할 수 있으므로, 이미 해제된 인스턴스에 접근할 일은 없어진다)

  • nil이 할당될 수 있어야 하니 반드시 optional 타입이 되어야 하며, 도중에 nil이 들어올 수 있으니(값이 바뀔 수 있으니) 반드시 변수(var)로 선언되어야 한다

  • 참고로 ARC가 weak 참조에 nil을 할당할 때 프로퍼티 옵저버는 불리지 않는다

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) deinit") }
}


class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    weak var tenant: Person? // 앞의 예제와 이 부분이 다르다!
    deinit { print("아파트 \(unit) deinit") }
}

var john: Person?
var unit4A: Apartment?

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

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

john = nil // print "John deinit"

unit4A = nil // print "아파트 4A deinit"

참고로 john=nil 후에 unit4A=nil 을 하든, unit4A=nil 후에 john=nil 을 하든, 순서에 상관없이 항상 john이 먼저 deallocated된다. (Person이 Apartment를 strong으로 잡고 있으니까, Person이 죽기 전까지는 Apartment도 죽지 않는다. deallocated를 위해서는 strong으로 참조가 하나도 걸려있지 않아야 한다)

<note> 가비지 콜렉션을 쓰는 시스템에서는 이런 weak 속성을 캐싱 구현에 사용하는 경우가 있다. 이런 시스템에서는 메모리 pressure가 발생해야만 가비지 콜렉터가 weak 참조가 걸린 것들을 해제하기 때문이다. 그러나 Swift의 ARC는 strong 참조가 0이 되자마자 메모리를 해제하기 때문에 이런 목적으로 쓰이기에 적합하지 않다.


<Unowned 참조>

  • 미소유 참조. weak참조와 마찬가지로 strong 참조가 아니다.

  • 프로퍼티나 변수 선언할 때 unowned 키워드를 앞에 붙이면 unowned 참조

  • 참조대상이 도중에 nil이 될 일이 없이 항상 값을 가지고 있을 때만 사용한다. 따라서 non-optional 타입.

  • optional 타입이 아니기 때문에 접근할 때 unwrap할 필요도 없고, 인스턴스가 해제될 때 ARC가 nil을 할당할 수도 없다 (weak 참조와 다른 점)

  • 이미 해제된 인스턴스를 참조하고 있던 unowned 참조에 접근하려 하면 runtime error (always crash). 이럴 일이 없을 경우에만 unowned 참조를 사용하자.


unowned 참조를 다음 예제로 알아보자.

  • Customer 클래스 : 은행 고객

  • CreditCard 클래스 : 고객에게 발급되는 신용카드

  • 서로의 인스턴스를 참조하고 있어야 하기 때문에 strong reference cycle의 위험성

  • 고객은 신용카드를 가질 수도 안 가질 수도 있지만 (optional) 신용카드는 반드시 고객과 연관이 되어야 한다 (nonoptional)

  • 신용카드 인스턴스는 항상 number 값과 고객 인스턴스가 있어야 생성될 수 있다. 따라서 신용카드 인스턴스는 항상 고객 인스턴스를 가진다고 보장되기 때문에 -> 신용카드 인스턴스는 고객을 unowned 참조로 가질 수 있다.

class Customer {
    let name: String
    var card: CreditCard?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) deinit")
    }
}


class CreditCard {
    let number: UInt64
    unowned let customer: Customer // strong 참조가 아니다
    
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    
    deinit {
        print("Card #\(number) deinit")
    }
}


var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, 
           customer: john!)

john = nil
// print "John Appleseed deinit"
// (Customer Instance를 strong으로 참조하는 녀석이 하나도 없어서 해제)
//print "Card #1234_5678_9012_3456 deinit"
// (CreditCard Instance도 마찬가지)


<note> 우리가 본 예제는 safe한 unowned의 경우이다. 그러나 퍼포먼스 상의 이슈로 런타임 때 unowned 참조가 safe하지 않게 될 수도 있다. (참조대상이 도중에 메모리가 해제되어 버림) unowned(unsafe) 라고 써주면 unsafe한 unowned 참조를 말하는 것이다. 자세한 것은 문서에 나와있지 않아서 구글링을 해본 결과 아래 링크와 같은 의견을 찾았다.

http://stackoverflow.com/questions/26553924/what-is-the-difference-in-swift-between-unownedsafe-and-unownedunsafe



<Unowned 참조 & 암시적으로 언랩핑된 옵셔널 프로퍼티Implicitly Unwrapped Optional Properties>

지금까지, 두 클래스가 서로에 대한 참조를 가지면 strong reference cycle이 발생할 수 있기 때문에 weak 참조나 unowned 참조를 사용해야 한다는 것을 알아보았다. 두 개의 클래스가 독립적으로 사용될 수 있는 경우. 앞선 예제의 아파트-사람같이 서로 옵셔널인 경우에는 weak, 하나에 디펜던시가 걸려있는 경우에는 unowned를 쓴다. 그런데 그 외의 케이스도 존재할 수 있다.

  • [Case1] 서로 nil이 될 수 있을 때 -> weak 참조

  • [Case2] 한 쪽만 nil이 될 수 있을 때 -> unowned  참조 (사라질수있는 클래스에서 상대방을 unowned 로 참조)

  • [Case3] 그럼 둘 다 nil이 될 수 없는 경우는 -> ???


서로를 참조하는 두 개가 모두 항상 값을 가지고 있으며 nil이 될 수 없는 케이스가 있다. 이때 유용히 쓸 수 있는 방법은 한 쪽 클래스는 Unowned 참조를 하고, 다른 한 쪽 클래스는 암시적으로 언랩핑된 옵셔널 프로퍼티Implicitly Unwrapped Optional Properties 가지는 방법이다. 이 방법을 사용하면 초기화가 된 후에 둘 다 언랩핑 없이 직접적으로 접근할 수 있고, 참조 사이클도 방지할 수 있다.


예제를 통해 자세히 알아보자.

  • Country 클래스 : 나라

  • City 클래스 : 수도

  • 모든 나라는 반드시 수도가 있어야 하고, 모든 수도는 반드시 나라에 포함되어야 한다.

  • 서로를 참조하고 있어야 하기 때문에 strong reference cycle의 위험성

class Country {
    let name: String
    var capitalCity: City!
    
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
        
        // 클래스 간의 상호의존을 위해 Country의 init 안에서
        // capitalCity(City 인스턴스)를 생성한다
        // 이 구조에서는 항상 Country를 먼저 만들어야 City를 만들 수 있다
        
        // 이니셜라이저 챕터에서 설명한 것처럼
        // capitalCity의 이니셜라이저에 넘길 self는,
        // 자기자신의 1단계 초기화가 끝날때까지(속성에 값이 다 들어갈 때까지)
        // 유효하지 않다
        // 따라서 capitalCity를 암시적 언랩핑 옵셔널(!)로 설정하여
        // (1) 초기화 1단계 시점에서 capitalCity가 nil이어도 괜찮도록 하고
        // (2) 추후 capitalCity에 접근할 때 언랩핑할 필요가 없게 만들고
        // (3) 결과적으로 클래스 간의 상호의존성을 설정하면서
        // (4) strong 참조 사이클도 방지할 수 있다
    }
}


class City {
    let name: String
    unowned let country: Country
    
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}


var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)의 수도는 \(country.capitalCity.name)")



클래스 인스턴스와 클로저 간의 Strong Reference Cycle

Strong reference cycle은 클래스 인스턴스 사이에만 생기는 것이 아니다. 클래스 인스턴스와 그 인스턴스의 프로퍼티에 할당된 클로저 사이에도 생길 수 있다. 클로저도 클래스와 마찬가지로 참조 타입이기 때문에 클로저를 프로퍼티에 할당할 때는 참조를 할당하게 되고, 그래서 strong reference cycle이 생길 수 있는 것이다.

만약 클로저 안에서 인스턴스의 프로퍼티에 접근하거나(ex: self.someProperty) 인스턴스의 메서드를 호출할 경우(ex: self.someMethod()) 클로저는 인스턴스를 캡쳐하게 되는데, 이때 strong reference cycle이 생긴다.

클로저의 value capture에 대해서는 이전 클로저 챕터를 참조해주세요.
( Closures 포스트 : http://wlaxhrl.tistory.com/40 )

Swift는 이런 경우를 해결하기 위해 클로저 캡쳐 리스트라는 우아한 해결책을 제공하고 있다. 하지만 해결책을 알아보기 앞서, 어쩌다가 strong reference cycle이 생기는지를 예제로 알아보자.

class HTMLElement {
    let name: String
    let text: String?
    
    // HTML 렌더링 방법을 바꾸고 싶으면 asHTML에 custom 클로저를 대입하기
    lazy var asHTML: () -> String = {
        // self를 여러번 참조하고 있다
        // (그래도 strong 참조는 한 번만 획득한다)
        if let text = self.text {
            return "<\(self.text)>\(self.text)</\(self.text)>"
        } else {
            return "<\(self.name) />"
        }
    }
    
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    
    deinit {
        print("\(name) deinit")
    }
}


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

paragraph = nil



<해결> 클래스 인스턴스와 클로저 간의 Strong Reference Cycle

클래스 인스턴스와 클로저 간의 Strong reference cycle은 클로저 정의의 일부로서 캡쳐 리스트capture list를 정의하여 해결할 수 있다. 캡쳐 리스트를 통해 클로저 바디에 한 개 이상의 참조가 사용될 때의 룰을 정의할 수 있는데, 참조를 strong 참조가 아니라 weak/unowned 참조로 정의할 수 있다. 어떤 참조를 쓸지는 코드에 따라 달렸다.

<note> 클로저 바디 안에서는 self.someProperty나 self.someMethod() 처럼 self를 명시적으로 써주어야 하는데, 이것을 통해 의도치 않은 캡쳐를 방지할 수 있다.


<Capture List 정의하기>

캡쳐 리스트의 각 아이템은 weak/unowned 키워드가 붙은 참조의 쌍이다. 이때 참조 대상은 클래스 인스턴스(ex: self) 혹은 초기화 된 변수(ex: delegate = self.delegate!) 등이 될 수 있다.

클로저의 파라미터와 리턴 타입을 써주고 그 앞에 캡쳐 리스트를 위치시키도록 한다. 리턴 타입은 없다면 생략 가능하다.

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


// 클로저의 파라미터와 리턴타입이 문맥에서 유추 가능하여 명시되지 않았을 경우에는
// 캡쳐 리스트를 이렇게 in 키워드 앞에만 위치시켜도 된다.
lazy var someClosure: () -> String = {
    [unowned self, weak delegate = self.delegate!] in
    // closure body goes here
}


<Weak/Unowned 참조>

  • 클로저와 클로저가 캡쳐할 인스턴스가 언제나 서로를 참조할 때, 캡쳐할 인스턴스가 nil이 될 일이 없을 때 -> unowned 참조를 사용. 항상 동시에 해제된다. (두 개의 라이프사이클이 동일한 경우이다)

  • 클로저가 캡쳐할 인스턴스가 어느 미래 시점에 nil이 될 수 있을 때 -> weak 참조를 사용. optional 타입. 인스턴스가 해제되면 nil이 할당된다. 따라서 클로저 바디 안에서 nil 체크를 하여 해제된 인스턴스에는 접근하지 않을 수 있다.

class HTMLElement {
    let name: String
    let text: String?
    
    lazy var asHTML: () -> String = {
        [unowned self] in // self를 unowned 참조로
        if let text = self.text {
            return "<\(self.text)>\(self.text)</\(self.text)>"
        } else {
            return "<\(self.name) />"
        }
    }
    
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    
    deinit {
        print("\(name) deinit")
    }
}


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

paragraph = nil
// print "p deinit"

'Swift 공식 가이드 > Swift 3' 카테고리의 다른 글

Error Handling  (0) 2017.03.18
Optional Chaining  (0) 2017.03.18
Deinitialization  (0) 2017.03.13
Initialization (3/3)  (0) 2017.03.13
Initialization (2/3)  (0) 2017.03.13