👨🏻‍💻iOS 공부/iOS & Swift

[Swift] ARC (Automatic Reference Counting)

728x90
반응형

 

ARC (Automatic Reference Counting)

Swift는 ARC를 사용하여 앱의 메모리 사용을 관리하고 추적한다. 대부분의 경우, Swift에서 메모리 관리란 "그냥 되는 것" 정도이고, 메모리 관리를 위해 별도로 생각할 필요가 없다. 왜냐하면 ARC가 자동적으로 쓸모가 없어진 클래스 인스턴스를 메모리에서 지워버리기 때문이다!

 

하지만, 몇몇 경우에 ARC는 메모리를 관리하기 위한 코드들 간의 관계에 대한 더 많은 정보를 요구하기도 한다. 이번 장에서는 ARC가 모든 앱의 메모리를 관리할 수 있도록 하는 것을 보여줄 것이다.

 

레퍼런스 카운팅(Reference counting)은 클래스의 인스턴스에게만 적용된다. 구조체나 열거형은 참조 타입이 아닌 값 타입이기 때문에 참조에 의해 저장되거나 보내질 수 없다!

How ARC Works

클래스의 인스턴스를 생성할 때 마다, ARC는 인스턴스에 대한 정보를 저장하기 위해 메모리 덩어리를 할당한다. 이 메모리는 인스턴스의 타입에 대한 정보 뿐만 아니라, 인스턴스와 관련된 저장된 프로퍼티(stored property)의 값들도 가지고 있다.

 

추가적으로, 인스턴스가 더 이상 필요하지 않게 되면, ARC는 인스턴스가 사용한 메모리를 비우고, 다른 목적으로 사용할 수 있도록 도와준다. 즉 클래스 인스턴스가 필요없어진다면, 더 이상 메모리 공간을 차지하지 않게 되는 것이다.

 

하지만, 아직 사용 중인 인스턴스를 만약 ARC가 해제해버렸다면, 해당 인스턴스의 프로퍼티에 접근할 수 없고, 인스턴스의 메서드 또한 호출할 수 없다. 만약 이러한 상황에서 인스턴스에 접근한다면, 앱은 크래쉬가 나게 된다! 왜냐? 메모리에 없는 상태이기 때문이다!

 

인스턴스가 아직 필요할 때 메모리에서 사라지지 않게 하기 위해, ARC는 각 클래스 인스턴스를 현재 참조하는 프러퍼티, 상수, 변수들의 수를 추적한다. ARC는 하나의 참조라도 인스턴스에 존재한다면 해당 인스턴스를 해제하지 않는다.

 

이를 가능하게 하려면, 클래스 인스턴스를 프로퍼티, 상수 또는 변수에 할당했을 때, 프로퍼티, 상수 또는 변수가 인스턴스에 강한 참조(strong reference)를 하도록 만들어줘야 한다. 인스턴스를 강하게 붙잡고, 강한 참조가 남아있다면 해제하는 것을 허용하지 않기에 "강한" 참조라고 부른다.

ARC in Action

이제 ARC가 어떻게 작동하는지 알아보자. 아래의 예시는 name 이라고 하는 프로퍼티를 가진 Person 클래스를 보여준다.

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

Person 클래스는 인스턴스의 name 프로퍼티를 세팅하는 이를 메세지로 출력하는 이니셜라이저를 가지고 있다. 또한 클래스의 인스턴스가 해제되었을 때 메세지를 출력하는 디이니셜라이저를 가지고 있다.

 

아래에는 Person? 타입의 세 변수가 있다. 옵셔널 타입이기 때문에, 자동으로 nil 값으로 초기화되고, 아직은 Person 인스턴스를 참조하지 않는다.

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

이제 Person 객체(인스턴스)를 만들 수 있다.

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

Person 클래스의 이니셜라이저가 호출되었기 때문에 "John Appleseed is being initialized"가 출력되는 것을 볼 수 있다. 이는 이니셜라이저가 제 때 잘 작동했다는 것을 의미한다.

 

새로운 Person 객체가 reference1 변수에 할당되었기 때문에, reference1 -> Person 으로의 강한 참조가 생긴다. 아직 1개의 강한 참조가 있기에 Person은 메모리에서 해제되지 않는다.

 

같은 Person객체를 다른 두 개의 변수에 할당한다면, 두 개의 강한 참조가 해당 객체에 추가적으로 생기게 된다.

reference2 = reference1
reference3 = reference1

이로써 단일 Person객체에 강한 참조가 3개나 생기게 된다.

 

만약 두 개의 변수에 nil을 할당하여 두 개의 강한 참조를 끊는다고 하면, 아직 하나의 강한 참조가 남게 되며, 그렇기에 Person객체는 메모리에서 해제되지 않는다.

reference1 = nil
reference2 = nil

ARC는 마지막 강한 참조가 끊길 때까지 Person객체를 메모리에서 해제하지 않으며, 메모리 해제를 하고 싶다면 마지막 참조 또한 끊어버리면 된다. 그러면 디이니셜라이저가 호출되는 것을 볼 수 있다.

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

Strong Reference Cycles Between Class Instances

위의 예시처럼, ARC는 Person 객체의 참조 횟수를 추적하고 더 이상 필요가 없다면 Person객체는 메모리에서 해제된다.

 

하지만 클래스 인스턴스의 참조 횟수가 0이더라도 이를 해제하지 않을 수 있는 방법이 있다. 바로 두 개의 클래스 인스턴스들이 서로를 강한 참조로 가지고 있는 경우 참조 횟수가 0이 되어도 메모리에서 해제되지 않는데, 이를 강한 순환 참조(Strong Reference Cycle)이라고 한다.

 

강한 순환 참조는 클래스들 간의 관계를 강한 참조 대신 weak 또는 unowned 참조하여 해결할 수 있다. 해결법은 아래에서 차차 알아보도록 하고, 우선 강한 순환 참조가 왜 일어나는지 그 원인을 이해해보자.

 

아래의 예시는 어쩌다가 생길 수 있는 강한 순환 참조에 대한 예시이다. PersonApartment라는 두 개의 클래스를 정의하고 있다.

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

모든 Person 객체는 문자열 타입의 name 프로퍼티를 가지고 있고 초기값이 nilapartment라는 옵셔널 프로퍼티를 갖고 있다. 사람이 항상 아파트를 보유하고 있지는 않기 때문에...(ㅠ) apartment는 옵셔널로 표기한다.

 

비슷하게, Apartment 객체는 문자열 타입의 unit 프로퍼티와 초기값이 niltenant 프로퍼티를 갖고 있다. 마찬가지로 아파트의 주인이 항상 있지는 않기 때문에 tenant는 옵셔널로 표기한다.

 

두 개의 클래스는 클래스의 인스턴스가 디이니셜라이즈 된다는 메세지를 갖고 있는 디이니셜라이저를 정의하고 있다. 이를 통해 PersonApartment가 메모리에서 해제되었는지 알 수 있다.

 

아래의 코드는 johnunit4A라고 하는 옵셔널 변수를 보여준다. 마찬가지로 옵셔널이기에 초기값은 nil 이다.

var john: Person?
var unit4A: Apartment?

이제 Person객체와 Apartment 객체를 johnunit4A에 할당할 수 있다.

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

두 객체를 생성하고 할당 했을 때의 강한 참조를 봐보자. john 변수는 이제 새로운 Person객체에 강한 참조를 갖고 있고, unit4A 변수는 새로운 Apartment 객체에 강한 참조를 갖고 있다.

 

이제 person이 apartment를 갖도록, apartment가 tenant를 갖도록 하여, 두 객체를 서로 연결할 수 있다. johnunit4A는 옵셔널로 정의되어 있기 때문에 프로퍼티에 접근하려면 느낌표(!)를 붙여서 언래핑해줘야 한다.

 

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

두 객체를 연결한 이후 강한 참조 관계는 다음과 같이 바뀐다.

 

불행하게도, 두 객체를 연결하게되면 이 둘 사이에서 강한 순환 참조 가 생기게 된다. Person 객체는 이제 Apartment 객체에 강한 참조를 가지고, Apartment 객체는 이제 Person 객체에 강한 참조를 가지게 된다. 그러므로, johnunit4A 변수의 강한 참조를 끊어도 (레퍼런스 카운트가 0이 되어도) 객체는 ARC에 의해 메모리 해제되지 않는다.

john = nil
unit4A = nil

마찬가지로 디이니셜라이저도 호출되지 않는다. 강한 순환 참조는 PersonApartment 객체가 메모리 해제되는 것을 방지하는데, 이 때문에 앱에서 메모리 누수(memory leak)가 발생하게 된다.

 

메모리 누수(memory leak) : 프로그램이 불필요한 메모리를 계속 점유하고 있는 현상. 할당된 메모리를 사용한 다음 반환하지 않는 것이 누적되면 메모리가 낭비된다. 즉 더 이상 불필요한 메모리가 해제되지 않으면서 메모리 할당을 잘못 관리할 때 발생한다.

 

johnunit4Anil을 할당했을 때 강한 참조는 아래와 같이 변경된다.

PersonApartment 객체 사이의 강한 참조는 끊어지지 않고 계속 유지되게 된다.

Resolving Strong Reference Cycles Between Class Instances

Swift는 클래스 타입의 프로퍼티로 작업하고 있을 때, 강한 순환 참조를 해결하기 위한 두 가지 방법을 제공하고 있다. 바로 weak 참조와 unowned 참조이다.

 

weak 참조와 unowned 참조는 인스턴스가 강한 참조 없이 다른 인스턴스를 참조할 수 있도록 만들어준다. 즉, 강한 참조를 하지 않기에 강한 순환 참조를 만들지 않고도 객체는 참조할 수 있는 것이다.

 

다른(참조할) 객체가 더 짧은 생애주기(lifetime)을 가지고 있다면 weak을 사용한다. (왜냐하면 다른 객체가 먼저 해제될 수 있기 때문이다.) Apartment 예시에서, 특정 시점에 apartment는 tenant를 가지고 있지 않을 수 있다. 그래서 이 경우에는 순환 참조를 깨기 위해서는 weak가 적절한 해결법이 되는 것이다. 반대로, unowned 참조는 다른 객체가 동일한 생애주기를 갖거나 더 긴 생애주기를 가질 때 사용한다.

Weak References

weak은 객체에 대한 강한 참조 없이 참조할 수 있게 해주고, 이 덕분에 ARC가 참조된 객체를 처리하는 것을 멈추지 않게 해준다. (= 메모리 해제를 방해하지 않는다.) 이러한 행위들이 강한 순환 참조를 막아준다. 프로퍼티나 변수의 선언 전에 weak 키워드를 써주면 약한 참조(weak reference)를 사용할 수 있다.

 

약한 참조는 객체에 대해 강한 참조를 하지 않기 때문에, 아직 약한 참조가 있는 와중에도 객체가 메모리 해제될 수 있다. 그러므로, ARC는 객체가 메모리 해제되면, 자동적으로 약한 참조를 nil로 세팅한다. 그리고 약한 참조는 런타임에 값이 nil로 변할 수 있음을 허용해줘야 하기 때문에 항상 상수(let) 보다는 옵셔널 타입의 변수(var)로 선언해줘야 한다.

 

기본적인 옵셔널 값에 접근하는 것 처럼 약한 참조에 값이 있는지 확인할 수 있고, 더 이상 존재하지 않는 객체에 대해서도 참조할 수 없다.

 

ARC가 약한 참조를 nil로 세팅했을 때, 프로퍼티 감시자(property observer)는 호출되지 않는다.

 

아래의 예시는, 위 Person, Apartment 예시와 같으나 중요한 차이가 하나 있다. 바로 Apartmenttenant가 약한 참조로 선언되었다는 것이다.

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

두 개의 변수(john, unit4A)로 부터의 강한 참조와, 두 객체 사이의 연결이 이전처럼 생성된다.

var john: Person?
var unit4A: Apartment?

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

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

이제 참조 관계가 어떻게 되었는지 살펴보자.

 

Person 객체는 여전히 Apartment 객체에 대해 강한 참조를 가지고 있지만, Apartment 객체는 Person객체에 대해 약한 참조를 가지고 있다. 이는 즉, john 변수를 nil로 세팅하여 강한 참조를 깼을 때, Person 객체에 대한 강한 참조는 더 이상 없게 된다.

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

이제 Person에 대한 강한 참조가 없기 때문에, 이는 메모리 해제되고 tenantnil이 된다.

 

여전히 남아있는 강한 참조는 unit4AApartment 객체를 참조하고 있는 것 뿐이다. 만약 이 강한 참조 또한 끊어버린다면, Apartment 객체에 대한 강한 참조는 더 이상 없게 된다.

 

unit4A = nil
// Prints "Apartment 4A is being deinitialized"

마찬가지로, Apartment 객체에 대한 강한 참조가 남아있지 않기에 메모리에서 해제된다.

 

 

가비지 컬렉션(garbage collection)을 사용하는 시스템에서는, 강한 참조가 없는 객체는 메모리 압력(memory pressure)이 가비지 컬렉션을 촉진할 때만 메모리 해제되기 때문에, weak 포인터는 종종 간단한 캐싱 매커니즘을 만들 때 사용된다. 하지만 ARC를 사용하면 마지막 강한 참조가 제거되는 즉시 값이 메모리 해제되므로, 약한 참조가 이러한 목적으로는 적합하지 않다.

Unowned References

약한 참조와 유사하게, 미소유 참조는 객체에 대해 강한 참조를 하지 않는다. 하지만 약한 참조와 다르게, 미소유 참조는 다른 객체가 동일한 생애주기 혹은 더 긴 생애주기를 갖고 있을 때 사용된다. 미소유 참조는 프로퍼티나 변수의 선언 앞에 unowned를 붙여서 사용할 수 있다.

 

약한 참조와 다르게, 미소유 참조는 "항상 값을 가지고 있을거라고" 예상한다. 즉 값이 무조건 있을 것이라는 것이다. 그렇기 때문에 unowned가 붙은 값은 옵셔널이 될 수 없고, ARC가 미소유 참조의 값을 nil로도 세팅할 수 없다.

 

절대로 해제되지 않을 객체를 항상 참조할 때 unowned를 사용한다.

만약 객체가 해제된 이후 미소유 참조의 값에 접근한다면, 런타임 에러를 일으키게 된다.

 

다음은 은행 고객과 고객에게 가능한 신용카드를 모델로 정의한 CustomerCreditCard 클래스에 대한 예시이다. 두 개의 클래스들은 다른 클래스의 인스턴스를 프로퍼티로써 저장하고 있다. 이 관계는 강한 순환 참조를 일으킬 가능성이 있다.

 

CustomerCreditCard의 관계는 약한 참조에서 봤던 ApartmentPerson 예시의 관계와는 조금 다르다. 이 데이터 모델에서, 고객은 카드를 가지고 있을 수도 있고 아닐 수도 있지만, 신용카드는 항상 고객과 연관되어있다. 생애주기로 봤을 때, CreditCard 객체는 CreditCard가 참조하는 Customer 보다 더 오래 살 수 없다. 이를 표현하기 위해서 Customer 클래스는 card 프로퍼티를 옵셔널로 하고, CreditCard 클래스는 customer 프로퍼티를 미소유(옵셔널도 아니도록)로 선언해준다.

 

추가적으로, CreditCard 객체는 커스텀 초기화를 통해 number값과 customer 객체를 전달하여야만 생성할 수 있다. 이는 CreditCard 객체가 생성될 때, 항상 customer 객체를 가질 수 있도록 보장해준다.

 

신용카드는 항상 고객을 가져야하기 때문에, 강한 순환 참조를 피하기 위해 customer 프로퍼티를 미소유 참조로 정의한다.

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

 

 

32비트와 64비트 시스템에서 16자리 카드 넘버를 수용하기 위해 CreditCard 클래스의 number 프로퍼티는 Int 보다는 UInt64로 정의하였다.

 

이제 john이라는 옵셔널한 Customer을 정의해준다. 이 변수는 옵셔널이기에 초기에는 nil이다.

var john: Customer?

이제 Customer 객체를 만들 수 있고, 고객의 card 프로퍼티에 CreditCard 객체를 할당할 수 있다.

 

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

이제 참조가 어떻게 구성되고 있는지 봐보자.

 

Customer 객체는 이제 CreditCard에 대한 강한 참조를 가지게 되고, CreditCard 객체는 이제 Customer에 대한 미소유 참조를 가지게 된다.

 

미소유 참조이기 때문에, john 변수에 대한 강한 참조를 끊게 되면 더 이상 Customer가 가지는 강한 참조는 없게 된다.

Customer객체에 더 이상 강한 참조가 없기 때문에, 이는 메모리 해제된다. 그 이후, CreditCard 객체에 강한 참조가 없기 때문에 이 또한 메모리 해제된다.

john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"

john 변수에 nil이 할당되면서 Customer 객체와 CrreditCard 객체 가 디이니셜라이즈 되면서 메세지가 출력되는 것을 볼 수 있다.

 

위 예시는 safe한 미소유 참조에 대한 것이다. Swift는 성능상의 이유로 런타임에서 safety checks를 하지 않으려 할 때 unsafe한 미소유 참조 또한 제공한다. unsafe한 모든 작업과 마찬가지로 사용자는 해당 코드의 안전성을 검사해야한다.

unowned(unsafe)를 명시하여 unsafe한 미소유 참조를 사용할 수 있다. 만약 객체가 메모리 해제된 이후, unsafe한 미소유 참조에 접근하려고 한다면, 프로그램은 객체가 있던 메모리 위치에 접근하려고 시도하게 되는데, 이는 안전하지 않는 작업이다.

 

Unowned Optional References

클래스에 대한 옵셔널 참조를 미소유로 표기할 수 있다. ARC의 경우 미소유 참조와 약한 참조는 같은 맥락에서 사용될 수 있었다. 차이점은 미소유 참조를 사용할 때, 참조하고자 하는 객체가 항상 값을 가지고 있는지(유효한지) nil은 아닌지 확인해야 한다는 것이다.

 

특정 학부(department)가 제공하는 코스(courses)들을 예시로 봐보자.

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

Department는 학부가 제공하는 각 coures들과 강한 참조를 가진다. ARC 관점에서 보면 학부가 강의를 소유한다는 것이다.

 

Course는 두 개의 미소유 참조를 가지고 있는데, 하나는 department에 대한, 그리고 나머지는 학생이 들어야 하는 nextCourse에 대한 것이다. 또한 모든 코스는 학부에 속하기 때문에 department프로퍼티는 옵셔널이 아니다. 하지만, 어떤 코스들은 추천 강의가 없을 수 있기에, 즉 nextCoure 프로퍼티는 옵셔널이 된다.

 

이 클래스를 사용한 예시를 봐보자.

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]

위 코드는 1개의 department와 3개의 courses를 생성한다. intro와 intermediate 코스는 모두 다음 코스를 제공하기에 nextCourse 프로퍼티에 이를 저장하고 있고, 다음에 수강해야하는 코스에 대해 미소유 옵셔널 참조를 유지하고 있다.

 

ARC에서 미소유 옵셔널 참조는 미소유 참조와 동작 원리는 동일하나, 미소유 옵셔널 참조는 nil이 될 수 있다는게 차이점이다.

옵셔널이 아닌 미소유 참조처럼, nextCourse 가 항상 메모리 해제 되지 않은 코스를 참조하도록 보장해야한다. 이 경우, 예를 들어,department.courses에서 코스를 제거하는 경우, 지운 과정을 참조하는 다른 과정 역시 지워야 한다.

Unowned References and Implicitly Unwrapped Optional Properties

지금까지 봤던 약한 참조, 미소유 참조 예시를 정리해보자.

 

PersonApartment 예시는 두 개의 프로퍼티가 모두 nil이 될 수 있었고, 이는 강한 순환 참조를 야기할 가능성이 있었다. 이는 weak 참조로 해결되었다.

 

CustomerCreditCard 예시는 하나의 프로퍼티가 nil이 될 수 있었고 다른 프로퍼티는 nil이 될 수 없는 상황에서 강한 순환 참조를 야기할 가능성이 있었다. 이 경우는 unowned 참조로 해결되었다.

 

하지만, 세 번째 경우에는 두 개의 프로퍼티들은 모두 값을 가지고 있어야 하고, 초기화 이후에도 nil이 되면 안된다. 이 때는 암시적 언래핑과 미소유 참조를 함께 사용하면 해결할 수 있다.

 

이는 초기화 이후에 프로퍼티들에 옵셔널 언래핑 없이 바로 접근 가능하도록 해주고, 강한 순환 참조 또한 피하게 해준다.

 

CountryCity 클래스는 서로의 클래스를 프로퍼티로 가지고 있다. 모든 나라는 수도가 있고, 모든 수도는 속해있는 나라가 있어야 한다. 이를 표현하기 위해서 Country 클래스는 capitalCity 프로퍼티가 있어야 하고, City 클래스는 country 프로퍼티를 가지도록 한다.

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

두 클래스 간의 상호의존성을 설정하기 위해, City의 이니셜라이저는 Country 객체를 받고 이를 country 프로퍼티에 저장한다.

City의 이니셜라이저는 Country의 이니셜라이저에서도 호출된다. 하지만 Country의 이니셜라이저는 Country 객체가 완전히 초기화 될 때 까지,City의 이니셜라이저에 self를 넘길 수 없다.

 

그래서 CountrycapitalCity를 암시적 언래핑한 프로퍼티로 성정하여, nil을 기본 값으로 갖도록 한다. 옵셔널과 동일해보이지만, 따로 언래핑을 해줄 필요가 없다. (암시적으로 언래핑한 옵셔널은 값에 접근할 때마다 옵셔널 값을 강제 언래핑 하는 것과 같음)

capitalCitynil 값을 초깃값으로 가지고 있기에, name만 이니셜라이저에서 초기화하면, Country 객체는 완전히 초기화되었다고 볼 수 있다. 이는 곧 Country 이니셜라이저가 self 프로퍼티를 참조하고 넘겨줄 수 있다는 것을 의미한다.

 

정리해보면, CountryCity 객체를 강한 순환 참조 없이 한 번에 만들 수 있고, capitalCity 프로퍼티는 강제 언래핑 없이 바로 접근할 수 있다는 것이다.

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"

암시적 옵셔널을 통해 2-단계 이니셜라이저의 요구사항을 만족시킨 것이다. 또한 강한 순환 참조 또한 피하면서, capitalCity 프로퍼티는 초기화 이후 옵셔널이 아닌 값처럼 접근할 수 있다.

Strong Reference Cycles for Closures

지금까지 두 개의 클래스 인스턴스 프로퍼티가 서로 강한 참조를 가져서 강한 순환 참조가 생기는 경우를 봤다. 또한 강한 순환 참조를 끊기 위해 weakunowned를 어떻게 사용하는지도 봤다.

 

강한 순환 참조는 클래스 객체의 프로퍼티에 클로저를 할당하고, 바디부분에서 객체를 캡쳐(capture)하면 강한 순환 참조가 발생한다. 클로저의 바디에서 객체의 프로퍼티에 접근하면서 발생하게 되는 것이다.


self.somePropertyself.someMethod처럼 객체의 프로퍼티나 메서드에 접근하는 경우 클로저 자체가 "캡쳐"되어 강한 순환 참조를 발생시킨다.

 

강한 순환 참조는 클로저나 클래스 같은 참조 타입에서 발생한다. 클로저를 프로퍼티에 할당할 때, 클로저에 참조를 할당하는 것과 같다. 본질적으로 위와 같은 문제를 발생시킨다. 이번에는 두 클래스 객체 간의 문제가 아니라 클래스 객체와 클로져 사이에서 발생하는 문제에 대해서 알아볼 것이다.

 

Swift는 이를 해결하기 위해 클로저 캡쳐 리스트(closure capture list)라는 것을 제공한다. 하지만 이를 사용하기 전에 어떻게 순환 참조가 발생하는지 먼저 알아보자.

 

아래 예시는 클로저가 self를 참조하게 되면서 강한 순환 차조가 발생하는 경우이다. HTML의 개별 요소를 제공해주는 HTMLElement 클래스를 예시로 보자.

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

}

HTMLElement 클래스는 요소의 이름을 나타내는 name 프로퍼티를 정의한다. HTML에서 "h", "p" 같은 것들이다. 또한 요소에서 텍스트를 나타내는 text는 옵셔널로 정의한다.

 

asHTML이라는 지연 프로퍼티를 정의한다. 이 프로퍼티는 nametext를 조합하여 HTML 문자를 만드는 클로저를 참조로 한다. 이는 () -> String 타입으로 함수는 파라미터가 없고 문자열 값을 리턴한다는 것을 의미한다.

 

아래는 textnil일 수 있기에 기본값을 설정해둔 예이다.

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

불행히도 HTMLElement 객체와 클로저 간에 강한 순환 참조가 발생하게 된다.

객체의 asHTML 프로퍼티는 클로저에 강한 참조를 가진다. 클로저가 바디에서 self를 참조하면서, 클로저는 스스로를 캡쳐해버리게 되어 강한 순환 참조가 발생하게 된다.

 

paragraphnil을 할당하여 HTMLElement인스턴스에 대한 강한 참조를 끊어도, 클로저와 HTMLElement는 메모리 해제되지 않는다.

 

paragraph = nil

Resolving Strong Reference Cycles for Closures

클로저를 정의할 때 캡처 리스트(capture list)를 정의하여 객체와 클로저 간의 강한 순환 참조를 해결할 수 있다. 캡처 리스트는 클로저의 바디에서 하나 혹은 그 이상의 참조 타입을 캡쳐할 때 사용할 규칙을 정의한다. 마찬가지로 강한 참조 대신에 weak, unowned를 상황에 맞게 사용하여 강한 순환 참조를 피할 수 있다.

Defining a Capture List

캡처 리스트 내의 각 아이템은 클래스의 객체(self) 혹은 특정 값으로 초기화될 변수들(delegate = self.delegate)과 함께 weak, unowned를 붙여 쌍으로 이루어져 있다. 이 쌍들은 대괄호로 둘러싸여있고 쉽표로 구분된다.

예시를 봐보자.

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

파라미터 리스트나 반환값을 명시할 필요가 없다면 다음과 같이 정의한다.

lazy var someClosure = {
    [unowned self, weak delegate = self.delegate] in
    // closure body goes here
}

Weak and Unowned References

클로저와 캡처하는 객체가 항상 서로를 참조하고, 동시에 메모리에서 해제되는 경우 unowned 키워드를 사용하여 미소유 참조를 사용한다.

 

반대로, 캡쳐된 참조가 nil이 될 수 있다면 약한 참조를 사용한다. 약한 참조는 항상 옵셔널 타입이기 때문에 메모리에서 참조가 해제되면 자동적으로 nil이 된다.

 

위 HTML 예시는 unowned로 풀기 적합하다.

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

}

클로저 부분만 빼면 나머지 구현부는 동일하다. 이 경우, 캡쳐 리스트는 [unowned self]이며, 이는 강한 참조가 아닌 미소유 참조로 self를 캡쳐한다는 것을 의미한다.

 

나머지는 동일하게 생성하고 출력해볼 수 있다.

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

이제 참조는 다음과 같이 보이게 된다.

 

이제 클로저의 selfunowned이기 때문에 HTMLElement와 강한 참조를 유지하지 않는다. 만약 paragraph 변수를 nil로 하여 강한 참조를 끊는다면 HTMLElement는 메모리 해제되며, 디이니셜라이저 또한 호출된다.

 

paragraph = nil
// Prints "p is being deinitialized"

 


가비지 컬렉션과 ARC 비교 부분은 다른 글에서 다뤄보자!

 

Ref:

 

https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

728x90
반응형