[Swift] 자동 참조 카운팅 (Automatic Reference Counting)

민경준·2023년 12월 7일
post-thumbnail

Model the lifetime of objects and their relationships.
개체의 수명과 관계를 모델링한다.

원문 문서에 Automatic Reference Counting에 대한 설명으로 한줄로 적혀있는 문장이다.
사실 이 문장만 읽어서는 "이게 뭔 소리람?" 잘 이해가 안가지만 전체 문서를 읽어가며 이해하도록 해보자.

🌟 Declaration

Swift는 자동 참조 계수(ARC)를 사용하여 앱의 메모리 사용량을 추적하고 관리한다.
대부분의 경우, 이것은 Swift에서 메모리 관리가 "그냥 작동"한다는 것을 의미하며, 메모리 관리에 대해 스스로 생각할 필요가 없다.
ARC는 더 이상 그러한 인스턴스가 필요하지 않을 때 자동적으로 클래스 인스턴스에서 사용되는 메모리를 해제한다.

그러나 일부 ARC는 메모리를 관리하기 위해 코드의 각 부분 간의 관계에 대한 더 많은 정보를 요구한다.
참조 카운트는 클래스의 인스턴스에만 적용되며, 구조체 및 열거형은 참조 유형이 아닌 값 유형이며 참조로 저장 및 전달되지 않는다.


🌟 ARC 작동 방식 (How ARC Works)

새 클래스 인스턴스를 생성할 때마다 ARC는 해당 인스턴스에 대한 정보를 저장할 메모리를 할당한다.
이 메모리에는 해당 인스턴스가 할당된 저장 프로퍼티의 값과 인스턴스의 타입에 대한 정보가 저장된다.

그리고 인스턴스가 더 이상 필요하지 않을 때 ARC는 해당 인스턴스가 차지하는 메모리를 해제 하여 클래스 인스턴스가 더 이상 필요하지 않을 때 메모리의 공간을 차지하지 않도록 보장한다.

하지만 ARC가 사용 중인 인스턴스를 제거한다면, 더 이상 해당 인스턴스의 속성에 접근하거나 해당 인스턴스의 메서드를 호출할 수 없다. 실제로 해당 인스턴스에 액세스하려고 하면 앱이 충돌할 가능성이 높다.

그래서 ARC는 필요할 때 인스턴스가 사라지지 않도록 각 클래스 인스턴스에서 현재 참조하고 있는 많은 속성, 상수 및 변수를 추적한다. 해당 인스턴스에 대해 활성화 된 참조가 적어도 하나 이상 존재하는 한, ARC는 인스턴스의 할당을 해제하지 않는다.

이를 가능하게 하기 위해 존재 하는것이 "강한" 참조이다. 속성, 상수 또는 변수에 클래스 인스턴스를 할당할 때마다 강한 참조를 만든다. 해당 참조는 해당 인스턴스를 확고하게 유지하도록 하고 유지되는 한 해당 인스턴스가 할당 해제되지 않도록 한다.


🌟 ARC 사용 (ARC in Action)

다음은 ARC가 어떻게 작동하는지 예시를 보도록 하자.
Person이라는 이름의 클래스와 name이라는 이름의 저장 프로퍼티가 있다.

Person 클래스는 name 프로퍼티를 초기화 하고 "(name) is being initialized" 라는 메세지를 출력하는 이니셜라이저가 존재하며 인스턴스가 할당 해제될 때 "(name) is being deinitialized" 라는 메세지를 출력하는 디이니셜라이저 또한 존재한다.

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

다음은 세 개의 Person? 타입 변수를 통해 메모리 할당과 해제가 어떻게 이뤄지는지 알아보는 예제이다.
당연하게도 세 개의 변수 모두 초기화는 nil값으로 이뤄진다.

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

세 개의 변수 중 하나에 새로운 인스턴스를 만들고 할당해보자.

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

이니셜라이저를 호출하는 시점에 "John Appleseed is being initialized" 가 출력되고 있으므로 초기화가 되었음을 알 수 있다.
새로운 Person 인스턴스가 reference1에 할당 되면서 강한 참조가 이뤄졌고, 이는 Person 인스턴스가 메모리에 저장되고 할당 해제되지 않도록 한다.

같은 Person 인스턴스를 나머지 두 개의 변수에 할당 해보자.

reference2 = reference1
reference3 = reference1

이제 하나의 인스턴스에 두 개의 강한 참조가 더 발생해 총 세 개의 강한 참조가 만들어졌다.
다시 두 개의 변수에 nil을 할당해서 두개의 강한 참조를 끊어보자.

reference1 = nil
reference2 = nil

Person 인스턴스는 할당 해제되지 않았다.

ARC는 세 번째 강한 참조가 깨질때 까지 Person 인스턴스를 할당 해제시키지 않으며, 세 번째 변수에 nil을 할당하여 강한 참조를 깨면 그제서야 출력되는 메세지를 통해 인스턴스의 메모리 해제가 이뤄지는 것을 확인할 수 있다.

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

🌟 클래스 인스턴스 사이의 강한 참조 순환 (Strong Reference Cycles Between Class Instances)

위에 예제를 통해 ARC는 새로운 인스턴스를 생성하고 더 이상 필요 없을 때 할당을 해제하기 위해 참조의 수를 추적한다는 점을 알 수 있었다.

그러나 나도 모르는 사이 클래스의 인스턴스가 강한 참조를 깰 수 없도록 코드를 작성하게 될 수 도 있다. 두 클래스 인스턴스가 서로에 대한 강한 참조를 유지하여 각 인스턴스가 다른 인스턴스를 유지하는 경우 발생하게 된다. 이를 두고 강한 참조 순환 (strong reference cycle) 이라고 한다.

강한 참조 순환은 강한 참조 대신 약한 (weak) 또는 미소유 (unowned) 참조로 정의하여 해결 할 수 있다. 이 과정은 클래스 인스턴스 간의 강한 참조 순환 해결 (Resolving Strong Reference Cycles Between Class Instances) 에 담겨 있지만 그 전에 먼저 이런 강한 참조 순환이 왜, 어떻게 발생하는지 부터 이해하는것이 유용하다.

강한 참조 순환이 발생하는 예시를 보도록 하자.
이 예제는 아파트와 거주자의 블럭을 모델링 하는 PersonApartment 라는 2개의 클래스를 정의한다.

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 인스턴스는 String 타입의 name 프로퍼티와 초기값이 nil 인 옵셔널 apartment 프로퍼티가 존재한다.
person이 항상 apartment를 가지고 있지 않기 때문에 옵셔널로 정의했다.

유사하게 모든 Apartment 인스턴스는 String 타입의 unit 프로퍼티와 초기값이 nil 인 옵셔널 tenant 프로퍼티를 가지고 있다. apartment가 항상 tenant를 가지고 있지 않기 때문에 옵셔널로 정의했다.

두 클래스 모두 인스턴스가 할당 해제 됨을 출력하는 문구를 가진 디이니셜라이저를 정의했고, 이를 통해 PersonApartment 의 인스턴스가 예상대로 할당 해제되는지 여부를 확인할 수 있다.

다음은 ApartmentPerson 인스턴스를 할당할 johnunit4A 라는 옵셔널 타입의 2개의 변수를 가지고 확인해보자.

var john: Person?
var unit4A: Apartment?

각 변수에 PersonApartment 인스턴스를 생성하고 할당 해 주었다.

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

다음은 각 변수가 인스턴스를 강한 참조하고 있는 모습을 나타낸 그림이다.

이제 PersonApartment를 가지고, ApartmentPerson을 가지도록 하여 두 인스턴스를 연결하도록 하자.
johnunit4A가 옵셔널 변수이기 때문에 느낌표(!)를 통해 언래핑하고 접근하여 해당 인스턴스의 프로퍼티를 설정 했다.

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

다음은 두 인스턴스를 연결 한 이후에 강한 참조가 어떻게 동작하고 있는지 모습을 나타낸 그림이다.

불행하게도 이 두 인스턴스의 연결은 서로간의 강한 참조 순환을 생성한다. Person 인스턴스는 이제 Apartment 인스턴스에 대한 강한 참조를 가지고 Apartment 인스턴스는 Person 인스턴스에 대한 강한 참조를 가진다.

즉, 아래와 같이 johnunit4A 변수에 의해 생긴 가진 강한 참조를 깨트려도 참조 카운트는 0으로 떨어지지 않고 인스턴스는 ARC에 의해 자동으로 할당 해제되지 않는다.

john = nil
unit4A = nil

두 변수에 nil을 할당했지만 초기화 해제 구문은 호출되지 않았다. 강한 참조 사이클은 PersonApartment 인스턴스가 할당 해제되는 것을 방지하여 앱에서 메모리 누수를 유발한다.

다음은 johnunit4A 변수를 nil로 할당한 후에 강한 참조를 보여주는 그림이다.

PersonApartment 인스턴스 간의 강한 참조는 끊어지지 않아서 메모리에 할당이 해제되지 않는다.


🌟 클래스 인스턴스 간의 강력 참조 순환 해결 (Resolving Strong Reference Cycles Between Class Instances)

Swift는 클래스 타입의 프로퍼티와 작업할 때 강한 참조 순환을 해결하기 위해 2가지 방법을 제공한다.
: 약한 참조 (weak references)와 미소유 참조 (unowned references).

약한 참조와 미소유 참조를 사용하면 강한 참조 순환을 만들지 않고도 서로를 참조할 수 있다.

다른 인스턴스의 수명이 더 짧은 경우 즉, 다른 인스턴스가 먼저 할당 해제될 수 있을 때 약한 참조를 사용한다. 위의 Apartment 예제에서 apartment는 person이 항상 존재 하는것이 아니므로 tenant 변수에 약한 참조를 사용하는 것이 적절한 방법이다. 반대로 다른 인스턴스의 수명이 동일하거나 더 긴 경우에는 미소유 참조를 사용하는것이 적절하다.

🔥 약한 참조 (Weak Reference)

약한 참조 (weak reference) 는 참조하는 인스턴스를 강하게 유지하지 않는 참조이므로 ARC가 참조된 인스턴스를 처리하는 것을 중단하지 않도록 하고, 강한 참조 순환의 일부가 되는 것을 방지한다. 프로퍼티 또는 변수 선언 전에 weak 키워드를 위치시켜 약한 참조를 나타내도록 한다.

약한 참조는 참조하는 인스턴스를 강하게 유지하지 않기 때문에 참조하는 동안 해당 인스턴스가 할당 해제될 수 있다. 따라서 ARC는 참조하는 인스턴스가 할당 해제되면 프로퍼티에 nil을 자동으로 할당한다. 그리고 약한 참조는 런타임에 값을 nil로 변경하는 것을 허락해야 하므로 항상 옵셔널 타입의 상수(let)가 아닌 변수(var)로 선해야 한다.

다른 옵셔널 값과 같이 약한 참조에 값이 존재 하는지 여부를 확인 할 수 있고, 더 이상 존재하지 않으며 유효하지 않은 인스턴스에 대한 참조로 이어지지 않을 것이다.

노트
프로퍼티 관찰자는 ARC가 약한 참조를 nil로 할당할 때 호출되지 않는다.

아래의 예제는 위의 PersonApartment 예제와 동일하지만 한가지 중요한 차이점이 있다. 이번에는 Apartment 타입의 tenant 프로퍼티는 약한 참조로 선언했다.

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

두 변수 (johnunit4A)에서의 강한 참조와 두 인스턴스 간의 연결은 이전과 같이 생성된다.

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 인스턴스에 대해 강한 참조를 가지지 않기 때문에 할당 해제되고 tenant 프로퍼티는 nil로 할당된다.

Apartment 인스턴스에 대한 유일한 강한 참조는 unit4A 변수이다. 이 변수에 대해 강한 참조를 끊으면 Apartment 인스턴스에 대한 강한 참조는 더이상 없다.

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

Apartment에 대한 강한 참조가 더 이상 없으므로 인스턴스가 메모리에서 할당 해제 된다.

노트
가비지 콜렉션(Garbage Collection)을 사용하는 시스템에서는 메모리 부족 압박에 의해 가비지 콜렉션이 트리거 될 때만 강한 참조가 없는 객체가 할당 해제되기 때문에 간단한 캐싱 메커니즘을 구현하는데 약한 포인터가 사용되는 경우가 있다. 그러나 ARC를 사용하면 메모리 압박에 상관 없이 마지막 강한 참조가 제거되자마자 값이 할당 해제되어 약한 참조는 이러한 목적에 적합하지 않다.


🔥 미소유 참조 (Unowned References)

약한 참조와 마찬가지로 미소유 참조 (unowned reference) 는 참조하는 인스턴스를 강하게 유지하지 않는다. 그러나 약한 참조와 다르게 미소유 참조는 다른 인스턴스의 수명이 같거나 더 긴 경우에 사용된다. 프로퍼티 또는 변수 선언 전에 unowned 키워드를 위치시켜 미소유 참조를 나타낸다.

약한 참조와 달리 미소유 참조는 항상 값이 있는것으로 예상하여 사용한다. 결과적으로 미소유로 만들어진 값은 옵셔널로 만들어 지지 않고 ARC는 미소유 참조의 값을 nil 로 할당하지 않는다.

중요
앞으로 할당 해제될 일이 없는 인스턴스를 참조한다고 확신하는 경우에만 미소유 참조를 사용한다.
인스턴스가 할당 해제된 후에 미소유 참조의 값에 접근하려고 하면 런타임 에러가 발생한다.

다음 예제는 은행 고객과 고객에 대한 가능한 신용카드를 모델링하는 CustomerCreditCard 인 2개의 클래스를 정의한다.
이 두 클래스는 프로퍼티로 다른 클래스의 인스턴스를 각각 저장하게 되고, 이 관계는 강한 참조 사이클을 생성할 가능성이 있다.

CustomerCreditCard 간의 관계는 위의 약한 참조 예제와 약간 다르다. 이 데이터 모델에서 CustomerCreditCard를 가지고 있거나 가지고 있지 않을 수 있지만 CreditCard는 항상 고객과 연관되어 있다. 이것을 표현하기 위해 Customer 클래스는 옵셔널 card 프로퍼티를 가지지만 CreditCard 클래스는 미소유와 옵셔널이 아닌 customer 프로퍼티를 가집니다.

또한 새로운 CreditCard 인스턴스는 생성자에 number 값과 customer 인스턴스를 전달해야만 생성할 수 있다. 이렇게 하면 CreditCard 인스턴스가 생성될 때 항상 연관된 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") }
}

노트
CreditCard 클래스의 number 프로퍼티는 number 프로퍼티의 용량이 32비트와 64비트 시스템 모두에 16자리 카드번호를 저장할 수 있을만큼 충분하도록 Int가 아닌 UInt64 타입으로 정의한다.

다음은 특정 고객에 대한 참조를 저장하는데 사용되는 john 이라는 옵셔널 Customer 변수를 정의한다.

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 인스턴스에 대해 미소유 참조를 가지고 있다.

CreditCard 인스턴스가 미소유 참조로 customer를 정의 했기 때문에, john이 가진 강한 참조를 깨트리면 더이상 Customer 인스턴스에 대한 강한 참조는 존재하지 않게 된다.

그로인해 Customer 인스턴스가 할당 해제되고 CreditCard 인스턴스에 대한 강한 참조도 없어지기 때문에 자동으로 CreditCard 인스턴스도 할당 해제되게 된다.

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

위의 마지막 코드는 john 변수를 nil로 할당한 후에 Customer 인스턴스와 CreditCard 인스턴스 모두 deinitialized 메세지를 확인할 수 있다.


🔥 미소유 옵셔널 참조 (Unowned Optional References)

미소유 옵셔널 참조를 표기 할 수도 있다. 미소유 옵셔널 참조 (unowned optional reference)와 약한 참조는 모두 같은 컨텍스트로 표기 할 수 있다. 다만, 차이점은 미소유 옵셔널 참조는 사용하거나 객체를 참조할 때 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
    }
}
  1. Department는 학부에서 제공하는 각 수업에 강한 참조를 유지한다.
  2. Course는 해당 학부에 대한 정보와 다음 수업에 대한 2개의 미소유 참조를 가지고 있다.
  3. 모든 수업은 과의 부분이므로 department 프로퍼티는 옵셔널이 아니다.
  4. 그러나 일부 수업은 다음 수업이 없을수도 있기 때문에 nextCourse 프로퍼티는 옵셔널로 정의되었다.

다음은 위의 클래스를 사용하는 예제이다.

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]

위의 코드는 한개의 학부와 3개의 수업을 생성했고, introintermediate 수업 모두 nextCourse 프로퍼티에 다음 수업을 할당 했다. 이는 학생이 수업을 완료한 후 수강해야 하는 과정에 대한 미소유 옵셔널 참조를 유지한다.

미소유 옵셔널 참조는 강하게 유지하지 않으므로 ARC가 할당 해제하는것을 방지하지 못하고, nil이 될 수 있다는 점을 제외하면 ARC에서 미소유 참조에 대한 수행 방식과 동일하게 동작한다. 즉, weak 프로퍼티를 사용할 때 자동으로 nil이 할당 되던것과 다르게 optional unowned 프로퍼티는 직접 nil을 할당해서 참조를 깨트려줘야 한다.

그러면 여기서 궁금점이 하나 생긴다. 대체 unownedOptional을 붙여서 얻는 이점이 대체 뭘까? 하는것이다. weak 참조를 사용하면 관련 변수의 참조가 깨질때 ARC가 자동으로 nil까지 할당해줘서 더 안전한거 아닌가? 라고... 그래서 검색해 찾아본 내용은 아래와 같았다.

  1. 일반 unowned 프로퍼티를 사용할 때는 절때 메모리에서 먼저 해제 될 일이 없다는걸 보장하고 사용하지만 실제로 먼저 할당해제 된 경우에 따로 체크 할 방법이 없고 해당 프로퍼티에 접근하게 되면 에러가 발생하는데, Optional을 추가하고 해당 변수에 인스턴스가 할당 되어 있는지 체크하면 이런 위험을 방지 할 수 있기 때문이다.

  2. 그리고 weak로 참조하게 되면 관련 변수들을 ARC가 모두 추적(Track) 하고 있어야 하는데, 이것은 작업이 하나 늘어나게 되는 셈이고 오버헤드를 증가시키게 된다. 즉, 먼저 할당 해제 될 일이 없는데도 불구하고 weak 참조를 사용하는것은 추적비용을 낭비하는 것이다. 그러나 unowned로 참조하게 되면 ARC는 관련 변수가 할당해제 될 일이 없다고 보장 받았으니 추적(Track) 작업을 하지 않아도 되고 이는 시간/공간/계산의 복잡성을 낮출 수 있다는 이점을 얻을 수 있게 된다.

노트
옵셔널 값의 기본 타입은 Swift 표준 라이브러리의 열거형인 Optional이다.
그러나 값 타입에 unowned를 표기할 수 없다는 규칙에 대해 예외가 적용된다.


🔥 미소유 참조와 암묵적 언래핑된 옵셔널 프로퍼티 (Unowned References and Implicitly Unwrapped Optional Properties)

위에서 우리는 약한 참조와 미소유 참조에 대해 강한 참조 순환을 중단해야 하는 일반적인 2가지 시나리오를 다뤘다.

PersonApartment 예제는 둘 다 nil이 될 수 있는 프로퍼티가 강한 참조 순환을 유발할 수 있는 가능성을 보여줬고, 이 시나리오는 약한 참조를 통해 해결헸다.

CustomerCreditCard 예제는 nil이 허용되는 하나의 프로퍼티와 nil이 될 수 없는 프로퍼티가 강한 참조 순환을 유발할 수 있는 가능성을 보여줬고, 이 시나리오는 미소유 참조로 해결했다.

그러나 두 프로퍼티 모두 항상 값이 있으면서 초기화가 완료된 이후엔 nil이 되어서는 안되는 세번째 시나리오가 있다. 이 시나리오에서는 한 클래스의 미소유 프로퍼티를 다른 클래스에 암시적으로 언래핑된 옵셔널 프로퍼티와 결합하는 것이 유용하다.

아래의 예제는 프로퍼티로 다른 클래스의 인스턴스를 저장하는 CountryCity 2개의 클래스를 정의한다.
이 데이터 모델에서 모든 국가는 항상 수도를 가지고 있으므로 모든 도시는 항상 국가에 속해 있어야 한다.
이 점을 표현하기 위해 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 인스턴스에 대한 초기화가 완료가 되지 않으면 City의 생성자에 self를 전달 할 수 없다는 점이다.

이 요구사항을 처리하려면 CountrycapitalCity 프로퍼티를 타입 끝에 느낌표(City!)로 표기하여 암시적 언래핑된 옵셔널 프로퍼티로 선언해줘야 한다. 이렇게 선언하게 되면 다른 옵셔널과 같이 초기에 nil로 할당되지만 따로 언래핑 할 필요없이 값에 접근할 수 있다.

capitalCity는 기본으로 nil값을 가지게 되므로 생성자 안에서 name에 대한 값을 할당하는 즉시 새로운 Conutry 인스턴스가 완벽히 초기화 된 것으로 간주한다. 즉, name에 값을 할당하는 즉시 selfCity의 생성자에 파라미터로 전달할 수 있다는 의미이기도 하다.

이 모든 것은 강한 참조 순환을 만들지 않고 단일 구문으로 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"

🌟 클로저에 대한 강한 참조 사이클 (Strong Reference Cycles for Closures)

강한 참조 순환은 클래스 인스턴스의 프로퍼티에 클로저를 할당하고 해당 클로저의 본문에 인스턴스를 캡처하는 경우에도 발생할 수 있다. 인스턴스 캡처는 self.someProperty 라던가 self.someMethod()와 같이 인스턴스의 프로퍼티 혹은 메서드에 접근할 때 발생한다. 두 경우 모두 이러한 접근은 클로저가 self캡처하여 강한 참조 사이클을 생성하게 된다.

이 강한 참조 순환은 클래스와 같이 클로저가 참조 타입 (reference type)이기 때문에 발생한다. 프로퍼티에 클로저를 할당하게 되면 해당 클로저에 참조를 할당하게 되고, 클로저 또한 인스턴스를 참조하면서 서로 강한 참조를 유지하게 되는 시나리오이다.

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는 "h1", "p" 또는 "br"과 같이 요소의 이름을 나타내는 name 프로퍼티를 정의하고,
랜더링 될 텍스트를 나타내는 문자열을 설정할 수 있는 옵셔널 text프로퍼티를 정의하고 있다.

이 간단한 두 프로퍼티 외에도 asHTML이라는 지연 프로퍼티 (lazy property)를 정의하고 있는데,
이 프로퍼티는 nametext를 HTML 문자열 조각으로 결합하는 클로저를 참조하고 있다.

또, asHTMLtext의 존재여부에 따라 "<p> some text </p>" 또는 "<p />"를 반환하고 메서드와 비슷하게 이름이 지어지고 사용되지만, 클로저 프로퍼티이기 때문에 내용을 변경하기 원하면 사용자 정의 클로저로 asHTML 프로퍼티의 기본값을 대체할 수 있다.

예를 들어 asHTML 프로퍼티가 빈 HTML 태그를 반환하는 표현을 방지하기 위해 text 프로퍼티가 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 이 지연 프로퍼티라는 사실은 초기화가 완료되고 self가 존재할 때까지 접근할 수 없으므로 기본 클로저 내에서 self 를 참조할 수 있다는 의미이다.

HTMLElement 클래스는 name 인수와 필요하면 text 인수를 사용하여 새로운 요소를 초기화 하는 단일 생성자를 제공하고, 할당 해제 될 때 메세지를 출력하는 소멸자도 제공한다.

다음은 어떻게 HTMLElement 클래스가 새로운 인스턴스를 생성하고 어떻게 출력하는지 보여준다.

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

노트
위의 paragraph 변수는 강한 참조 순환의 존재를 보여주기 위해 아래에서 nil로 설정할 수 있게 해야 하므로 옵셔널 HTMLElement 클래스로 정의한다.

안타깝게도 위에서 작성한 HTMLElement 클래스는 인스턴스와 기본 asHTML 값으로 사용된 클로저 간에 강한 참조 순환이 발생한다. 아래는 그 순환을 나타내는 그림이다.

인스턴스의 asHTML 프로퍼티는 클로저에 대해 강한 참조를 유지하고 클로저는 본문 내에서 HTMLElement 인스턴스에 다시 강한 참조를 유지한다는 의미로 self를 캡처하면서 둘 사이에 강한 참조 순환이 생기게 된다.

캡처에 관한 자세한 내용은 캡처값 (Capturing Values)을 참고하기 바랍니다.

노트
클로저는 여러번 self를 참조하지만 HTMLElement 인스턴스에 대해 하나의 강한 참조만 캡처한다.

아래 코드에서 초기화 해제 구문의 메세지가 출력되지 않는것을 통해 parapraph 변수를 nil로 할당하더라도 강한 참조 순환 때문에 HTMLElement 인스턴스는 할당 해제 되지 않음을 확인할 수 있다.

parapraph = nil

🌟 클로저에 대한 강한 참조 사이클 해결 (Resolving Strong Reference Cycles for Closures)

클로저의 정의의 한 부분으로 캡처 리스트 (capture list)를 정의하여 클로저와 클래스 인스턴스 간의 강한 참조 순환을 해결할 수 있다. 캡처 리스트는 클로저의 본문 내에서 하나 이상의 참조 타입을 캡처할 때 사용할 규칙을 정하는데, 두 클래스 간의 강한 참조 순환과 마찬가지로 캡처된 각 참조를 강한 참조가 아닌 약한 참조 또는 미소유 참조로 선언한다.

🔥 캡처 리스트 정의 (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
}

클로저는 컨텐스트로 부터 유추할 수 있기 때문에 파라미터 리스트 또는 반환 타입을 지정하지 않으면 캡처 리스트는 클로저의 가장 처음에 위치하고 이어서 in 키워드가 온다.

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

🔥 약한 참조와 미소유 참조 (Weak and Unowned References)

클로저와 캡처한 인스턴스가 항상 서로를 참조하고 항상 같은 시간에 할당 해제될 때는 미소유 참조로 정의하고, 캡처된 참조가 향후에 nil이 할당 될 가능성이 있다면 약한 참조로 캡처를 정의한다.

노트
캡처된 참조가 nil이 되지 않으면 약한 참조보다 미소유 참조로 항상 캡처되어야 한다.

미소유 참조는 위의 클로저에 대한 강한 참조 사이클 (Strong Reference Cycles for Closures)HTMLElement 예제에서 강한 참조 순환을 해결하기 위해 사용할 적절한 캡처 방법이다. 다음은 참조 순환을 피하기 위해 HTMLElement 클래스를 어떻게 작성하는지 보여준다.

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

}

HTMLElement의 구현은 asHTML 클로저 내에서 캡처 리스트의 추가를 제외하면 이전 구현과 동일한데, 이러한 경우에 캡처 리스트는 강한 참조가 아닌 "미소유 참조"로 self를 캡처하므로 [unowned self]를 사용하면 된다.

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

다음은 캡처 리스트가 참조에서 어떻게 보이는지 나타내는 그림이다.

이번에는 미소유 참조로 선언했기 때문에 클로저에 의해 self의 캡처가 강하게 유지되지 않고, paragraph 변수를 nil로 할당하게 되면 아래와 같이 초기화 해제 구문의 메세지를 출력하며 인스턴스가 할당 해제 되는것을 확인할 수 있다.

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

클로저에 표현에 대한 더 자세한 내용은 클로저 표현식 (Closure Expression)을 참고 바랍니다.







Reference

profile
iOS Developer 💻

0개의 댓글