[Swift 공식문서 읽기]Automatic Reference Counting

안녕하세요. 엘림입니다🙇🏻‍♀️

Swift 공식 문서를 정독하기 시리즈입니다!

제 스타일대로 정리했으니 추가적으로 더 필요한 정보는
공식문서 링크를 눌러 확인해주세용!

좀 더 편하게 보기위해 한국어로 번역된 사이트를 함께 확인했습니다!ㅎㅎ

자, 그럼 시작해볼까요

이 글은 공부하면서 작성한 글이기 때문에 잘못된 정보가 있을 수 있습니다.🥺
금방 잊어버릴... 미래의 저에게 다시 알려주기 위한 글이다보니
혹시라도 틀린 부분이 있다면, 댓글로 친절하게 알려주시길 부탁드립니다.🙏


ARC - 자동 참조 카운트

Swift에서는 앱의 메모리 사용을 관리하기 위해 ARC(Automatic Reference Counting)을 사용합니다. 자동으로 참조 횟수를 관리하기 때문에 대부분의 경우에 개발자는 메모리 관리에 신경쓸 필요가 없고 ARC가 알아서 더이상 사용하지 않는 인스턴스를 메모리에서 해지합니다. 하지만 몇몇의 경우 ARC에서 메모리 관리를 위해 코드의 특정 부분에 대한 관계에 대한 정보를 필요로 합니다. 참조 횟수는 클래스 타입의 인스턴스에만 적용되고 값 타입인 구조체 열거형 등에는 적용되지 않습니다.

How ARC Works

클래스의 새 인스턴스를 만들자마자, ARC는 인스턴스 정보를 담는데 필요한 적정한 크기의 메모리를 할당합니다. 이 메모리는 그 인스턴스에 대한 정보와 관련된 저장 프로퍼티 값도 갖고 있습니다. 추가적으로 인스턴스가 더 이상 사용되지 않을 때 ARC는 그 인스턴스가 차지하고 있는 메모리를 해지해서 다른 용도로 사용할 수 있도록 공간을 확보해 둡니다. 하지만 만약 ARC가 아직 사용중인 인스턴스를 메모리에서 내렸는데 인스턴스의 프로퍼티에 접근한다면 앱은 아마 크래시가 발생하게됩니다. ARC에서는 아직 사용중인 인스턴스를 해지하지 않기 위해 얼마나 많은 프로퍼티, 상수 혹은 변수가 그 인스턴스에 대한 참조를 갖고 있는지 추적합니다. 그래서 ARC는 최소 하나라도 그 인스턴스에 대한 참조가 있는 경우 그 인스턴스를 메모리에서 해지 하지 않습니다.

ARC in Action

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?

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

reference2 = reference1
reference3 = reference1

이 경우 reference2, reference3이 모두 reference1이 참조하는 같은 Person 인스턴스를 참조하게 됩니다. 이 시점에 Person 인스턴스의 참조 횟수는 3이 됩니다.

reference1 = nil
reference2 = nil

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

참조하고 있는 변수들을 nil로 만들면 참조가 해지되고, 마지막으로 참조하고있던 변수까지 nil로 만들어주면, 모든 참조가 해제되어 메모리에서 내려가는 것을 확인할 수 있습니다.

강한 순환 참조

앞선 예제에서 보았다시피 ARC에서 기본적으로 참조 횟수에 대해 추적하고 있기 때문에 더이상 사용하지 인스턴스는 자동으로 메모리에서 해제되게 됩니다. 하지만 절대로 메모리에서 해제 되지 않는 경우도 있습니다. 예를들어, 클래스의 인스턴스간 강하게 상호 참조를 하고 있는 경우가 바로 그경우 입니다. 이 경우는 강한 참조 순환이라 알려져 있습니다. 예를 통해 어떻게 강한 참조 순환이 발생하는지 알아 보겠습니다.

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")
unit4A = Apartment(unit: "4A")

이 상황에서 서로를 할당해보겠습니다.

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

인스턴스 안의 apartment와 tenant가 각각 Apartment, Person 인스턴스를 참조하고 있는 상황이 됩니다 즉 Person 인스턴스의 참조 횟수는 2, Apartment의 인스턴스 참조 횟수도 마찬가지로 2가 됩니다.

이 시점에서 각 변수에 nil을 할당해 참조를 해지해 보겠습니다. 원래 의도한 것은 각 변수가 참조하고 있던 Person과, Apartment인스턴스가 해지되는 것이었을 것입니다. 그러나 이 두 인스턴스는 해지되지 않습니다.

john = nil
unit4A = nil

강한 순환 참조 해결

해결 방법으로는 week와 unowned가 있습니다. 둘 모두 ARC에서 참조 횟수를 증가시키지 않습니다.

약한 참조(week)

참조하고 있는 것이 먼저 메모리에서 해제되기 때문에, ARC는 약한 참조로 선언된 참조 대상이 해지 되면 런타임에 자동으로 참조하고 있는 변수에 nil을 할당합니다.(이 때, 프로퍼티 옵저버는 실행되지 않습니다.)

아까 했던 코드와 똑같지만 아래 부분처럼, Apartment안에 tenant에 weak를 붙이면 된다.

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

미소유 참조(unowned)

참조 대상이 되는 인스턴스가 현재 참조하고 있는 것과 같은 생애주기(lifetime)를 갖거나 더 긴 생애 주기(longer lifetime)를 갖기 때문에 항상 참조에 그 값이 있다고 기대됩니다. 그래서 ARC는 미소유 참조에는 절대 nil을 할당하지 않습니다. 다시말하면 미소유 참조는 옵셔널 타입을 사용하지 않습니다.

미소유 참조는 참조 대상 인스턴스가 항상 존재한다고 생각하기 때문에 만약 미소유 참조로 선언된 인스턴스가 해제됐는데 접근하게 되면 런타임 에러가 발생합니다.

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!)

john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
// 즉, 사람을 할당 해제하는 순간 card도 사라짐

카드가 먼저 사라질 수 있다고 생각한 경우입니다. 즉 unowned를 붙인 프로퍼티가 더 오래 살아있을 것이라고 예상한 것이죠.(nil이 될 일이 없다는 의미인 것 같습니다.)

옵셔널 미소유 참조

class Department {
    var name: String
    var courses: [Course]
    init(name: String) {
        self.name = name
        self.courses = []
    }
}

class Course {
    var name: String
    unowned var department: Department
    unowned var nextCourse: Course?
    init(name: String, in department: Department) {
        self.name = name
        self.department = department
        self.nextCourse = nil
    }
}

let department = Department(name: "Horticulture")

let intro = Course(name: "Survey of Plants", in: department)
let intermediate = Course(name: "Growing Common Herbs", in: department)
let advanced = Course(name: "Caring for Tropical Plants", in: department)

intro.nextCourse = intermediate
intermediate.nextCourse = advanced
department.courses = [intro, intermediate, advanced]


미소유가 nil이 될 수 있게되면서, weak와 사실상 다른 점은 없습니다. 다만 weak는 할당 해제가 되었을때 자동으로 nil을 할당해주지만, unowned는 그렇지 않으므로 직접 관리를 해주어야합니다. 대신 weak는 계속 해당 참조를 추적하고 있어야 하므로, 오버헤드가 발생할 수 있다고 합니다!

미소유 참조와 암시적 옵셔널 프로퍼티 언래핑

(???) 이 경우가 뭔지 이해가 안감, capitalCity에 ?을 붙여도 똑같은데..?

class Country {
    let name: String
    var capitalCity: City!
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}

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)'s capital city is called \(country.capitalCity.name)")
// Prints "Canada's capital city is called Ottawa"

클로저에서 강한 순환 참조

강한 참조 순환은 변수 뿐만아니라 클로저와 관계돼서 발생할수도 있습니다. 왜냐하면 클로저에서는 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")
    }
}

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

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


앞서 이야했던 것 처럼, 인스턴스(self)와 클로저 간에 강한 참조를 하게돼서 강한 순한 참조에 빠지게 됩니다.
(클로저 안에서 self를 여러번 참조하더라도 실제로는 단 한번의 강한 참조만 캡쳐합니다.)

클로저에서 강한 참조 순환 문제의 해결하기 위해 캡쳐 참조에 강한 참조 대신 약한 참조(weak) 혹은 미소유(unowend) 참조를 지정할 수 있습니다. 약한 참조인지 미소유 참조를 사용할지는 코드에서 상호 관계에 달려있습니다.
(Swift에서는 클로저에서 특정 self의 메소드를 사용할 때 캡쳐를 실수하는 것을 막기위해 someProperty 혹은 someMethod 대신 self.someProperty 혹은 self.someMethod와 같이 self를 명시하는 것을 필요로 합니다.)

캡처리스트 정의

캡처리스트를 정의하기 위해서는 클로저의 파라미터 앞에 소괄호([])를 넣고 그 안에 각 갭쳐 대상에 대한 참조 타입을 적어 줍니다.

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
}

약한 참조와 미소유 참조

앞서 인스턴스 참조와 마찬가지로 참조가 먼저 해제되는 경우는 약한 참조를, 같은 시점이나 나중 시점에 해제되는 경우에는 미소유 참조를 사용합니다.
(만약 캡쳐리스트가 절대 nil이 될 수 없다면 그것은 반드시 약한 참조 리스트가 아니라 미소유 참조 리스트로 캡쳐돼야 합니다.)

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

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


오늘도 스위프트 공식문서를 정리해보았군욥~
다음편도 힘내보겠습니다!

감사합니다🙇🏻‍♀️

좋은 웹페이지 즐겨찾기