본문 바로가기
프로그래밍/Swift

[Swift] Automatic Reference Counting (ARC)

by 별준 2022. 3. 10.

References

Contents

  • Automatic Reference Counting
  • Strong References
  • Strong Reference Cycles
  • Weak References
  • Unowned References
  • Unowned Optional References
  • Strong Reference Cycles for Closures

Swift는 Automatic Reference Counting(자동참조카운팅, ARC)를 사용해서 앱의 메모리 사용을 추적하고 관리합니다. 대부분의 경우, Swift 내에서 메모리 관리는 알아서 동작하고, 우리는 메모리 관리에 신경 쓸 필요가 없다는 것을 의미합니다. ARC는 클래스의 인스턴스가 더 이상 필요하지 않으면 그 인스턴스에서 사용하던 메모리를 자동으로 해제합니다.

 

그러나 몇몇의 경우 ARC는 메모리를 관리하기 위해서 작성된 코드 간의 관계에 대한 더 많은 정보를 요구합니다. 이번에는 앱의 메모리 전체를 ARC가 어떻게 관리하는 지와 그 상황들에 대해 알아보도록 하겠습니다.

여기서 레퍼런스 카운팅(Refernce Counting)은 오직 클래스의 인스턴스에만 적용됩니다. 구조체나 열거형은 참조 타입이 아닌 값 타입이기 때문에 레퍼런스로 저장되거나 전달되지 않습니다.

 


How ARC Works

클래스의 새로운 인스턴스를 생성할 때마다 ARC는 그 인스턴스에 대한 정보를 저장하기 위한 메모리 chunk를 할당합니다. 이 메모리는 인스턴스의 타입에 대한 정보를 가지고 있고 인스턴스와 관련된 저장 프로퍼티의 값들도 함께 가지고 있습니다.

 

이러한 인스턴스가 더 이상 필요하지 않을 때, ARC는 그 인스턴스에서 사용하던 메모리를 해제하여 해당 메모리가 다른 목적으로 사용될 수 있도록 합니다. 이는 클래스 인스턴스가 더 이상 필요없을 때 메모리 공간을 차지하지 않도록 보장합니다.

그러나, 만약 ARC가 여전히 사용 중인 인스턴스를 해제하면 더 이상 해당 인스턴스의 프로퍼티에 액세스하거나 메소드를 호출할 수 없습니다. 실제로 해제된 인스턴스에 액세스하려고 하면 앱이 크래시될 가능성이 큽니다.

 

여전히 필요한 인스턴스가 사라지지 않게 하기 위해서는 ARC는 많은 프로퍼티, 상수, 변수들이 현재 어떻게 각 클래스 인스턴스를 참조하는지 추적합니다. ARC는 여전히 존재하는 인스턴스의 참조가 적어도 하나라도 존재하면 그 인스턴스를 해제하지 않습니다. 이를 가능하게 하기 위해서 프로퍼티, 상수 또는 변수에 클래스 인스턴스를 할당할 때마다 해당 프로퍼티, 상수, 또는 변수는 그 인스턴스에 대한 강한 참조(strong reference)를 만듭니다. 강한 참조가 남아있는 한 인스턴스는 해제되지 않기 때문에 'strong' reference라는 이름으로 불립니다.

 


ARC in Action

다음 이어지는 예제 코드들은 ARC가 어떻게 동작하는지 보여줍니다. 이 예제는 Person이라는 간단한 클래스를 정의합니다.

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

Person 클래스에는 디이니셜라이저가 있는데 클래스의 인스턴스가 해제될 때 특정 메세지를 출력합니다.

 

다음 코드는 Person? 타입의 변수 3개를 정의하고, 이어지는 코드에서 새로운 Person 인스턴스를 여러 번 참조하도록 사용할 예정입니다. 이 변수들은 옵셔널 타입이기 때문에 자동으로 nil로 초기화되고 현재 Person 인스턴스를 참조하지 않는 상태입니다.

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

이제 새로운 Person 인스턴스를 생성하고, 3개의 변수 중에 하나에 할당합니다.

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

새로운 Person 인스턴스는 reference1 변수에 할당되었기 때문에 이제 reference1에서 새로운 Person 인스턴스로의 강한 참조가 발생합니다. 적어도 하나의 강한 참조가 존재하기 때문에 ARC는 이 Person 인스턴스를 메모리에 유지시키며 해제시키지 않습니다.

 

만약 동일한 Person 인스턴스를 나머지 두 변수에 할당하면, 2개의 강한 참조가 더 추가됩니다.

reference2 = reference1
reference3 = reference1

이제 하나의 Person 인스턴스에는 3개의 강한 참조가 존재합니다.

 

여기서 만약 원본 레퍼런스를 포함하여 2개의 강한 참조를 깨기 위해 두 변수에 nil을 할당한다면, 이제 하나의 강한 참조가 남게되고, Person 인스턴스는 아직 해제되지 않습니다.

reference1 = nil
reference2 = nil

 

ARC는 마지막 강한 참조가 깨질 때까지 Person 인스턴스를 해제하지 않습니다. Person 인스턴스를 더 이상 사용하지 않을 때 Person 인스턴스를 해제하고 메세지가 출력됩니다.

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

 


Strong Reference Cycles Between Class Instances

위에서 살펴본 예제에서 ARC는 생성한 Person 인스턴스의 참조 개수를 추적하고, 더 이상 그 인스턴스를 필요하지 않을 때 해제할 수 있습니다.

그러나, 강한 참조 개수가 0이되는 순간을 달성할 수 없는 클래스의 인스턴스를 작성할 가능성이 존재합니다. 이 문제는 두 클래스의 인스턴스가 서로를 강한 참조로 참조하여 각 인스턴스가 다른 인스턴스를 계속 active 상태로 유지하는 경우에 발생할 수 있습니다. 이러한 문제를 강한 참조 순환(strong reference cycle)이라고 합니다.

 

강한 참조 순환 문제는 클래스 간의 관계를 강한 참조 대신 약한 참조(weak reference) 또는 소유하지 않은 참조(unowned reference)로 정의하여 해결할 수 있습니다. 이 프로세스는 잠시 뒤 아래에서 자세히 살펴보겠습니다. 이러한 문제를 해결하는 방법을 알아보기 전에, 먼저 이러한 순환이 어떻게 발생하는지 살펴보겠습니다.

 

다음 예제 코드는 어떻게 강한 참조 순환이 발생하는지 보여줍니다. 여기서 Person과 Apartment라는 두 클래스를 정의하고 그 구현은 다음과 같습니다.

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 프로퍼티와 nil로 초기화되는 옵셔널 apartment 프로퍼티를 갖습니다. 비슷하게 모든 Apartment 인스턴스는 unit 프로퍼티와 nil로 초기화되는 옵셔널 tenant 프로퍼티를 갖습니다.

두 클래스는 모두 디이니셜라이저를 정의하고 있으며 클래스가 해제되는 순간 그 사실을 콘솔에 출력해줍니다.

 

다음 코드는 옵셔널 타입은 John과 unit4A라는 두 변수를 정의하며, 모두 nil로 초기화됩니다.

var john: Person?
var unit4A: Apartment?

이제 Person 인스턴스와 Apartment 인스턴스를 생성하고 각각의 변수에 새로운 인스턴스들을 할당합니다.

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

 

아래 그림은 두 인스턴스를 생성하고 할당한 후 강한 참조가 어떻게 형성되었는지 보여줍니다. 변수 John은 이제 새로운 Person 인스턴스에 강한 참조를 갖고 있고, uni4A는 Apartment 인스턴스에 강한 참조를 갖습니다.

이제 두 인스턴스를 함께 연결해서 person은 apartment를 갖고, apartment는 tanant를 갖도록 해보겠습니다. 느낌표는 john과 unit4A 옵셔널 변수 내 저장되어 있는 인스턴스를 언랩(unwrap)하고 액세스하는데 사용됩니다.

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

두 인스턴스가 연결되면 이제 강한 참조는 다음 그림과 같이 형성됩니다.

두 인스턴스 간의 연결은 강한 참조 순환(strong reference cycle)을 발생시킵니다. Person 인스턴스는 이제 Apartment 인스턴스에 강한 참조를 가지고 있고, Apartment 인스턴스는 Person 인스턴스에 대해 강한 참조를 갖습니다. 그러므로 john과 unit4A 변수들이 잡고 있는 강한 참조를 깨려고 하더라도, 레퍼런스 카운트는 0으로 떨어지지 않고 ARC에 의해서 인스턴스들이 해제되지 않습니다.

john = nil
unit4A = nil

위 코드처럼 두 변수를 nil로 설정했지만, 각 인스턴스의 디이니셜라이저가 호출되지 않습니다. 강한 참조 순환은 Person과 Apartment 인스턴스가 해제되는 것을 막고, 앱에서 메모리 누수를 일으킵니다.

john과 unit4A 변수에 nil을 할당한 후 강한 참조는 다음과 같이 아직 형성되어 있는 상태입니다.

따라서 아직 강한 참조가 Person 인스턴스와 Apartment 인스턴스에 존재하기 때문에 참조가 깨지지 않습니다.

 


Resolving Strong Reference Cycles Between Class Instances

Swift는 강한 참조 순환 문제를 해결하기 위해 2가지 방법을 제공합니다. 하나는 weak reference(약한 참조), 다른 하나는 unowned reference(미소유 참조) 입니다.

 

약한 참조와 미소유 참조는 참조 순환에서 하나의 인스턴스가 다른 인스턴스를 강하게 붙잡지 않고 참조할 수 있도록 합니다. 따라서 강한 참조 순환을 발생하지 않고 다른 인스턴스를 참조할 수 있습니다.

 

다른 인스턴스가 더 짧은 생애주기(lifetime)를 갖고 있을 때, 즉, 다른 인스턴스가 먼저 해제될 수 있는 경우에 약한 참조를 사용합니다. 위에서 살펴본 Apartment 예제 코드에서 apartment는 그 생애주기에서 어느 포인터에서 tenant가 없을 수 있기 때문에 참조 순환을 깨기 위해서 약한 참조를 사용하는 것이 적절합니다. 대조적으로 미소유 참조는 다른 인스턴스가 같거나 더 긴 생애주기를 가질 때 사용합니다.

 

Weak References

약한 참조는 참조하는 인스턴스를 강하게 붙잡지 않는 참조이며, 따라서 ARC가 참조된 인스턴스를 해제하는 것을 막지 못합니다. 이러한 동작은 참조가 강한 참조 순환이 되는 것을 막아줍니다. 약한 참조는 프로퍼티나 변수 선언 앞에 weak 키워드를 붙여서 약한 참조라는 것을 가리킬 수 있습니다.

 

약한 참조는 참조하는 인스턴스를 강하게 붙자고 있지 않기 때문에 약한 참조가 아직 그 인스턴스를 참조하고 있더라도 해당 인스턴스가 해제될 가능성이 있습니다. 그러므로 ARC는 약한 참조가 참조하는 인스턴스가 해제될 때 그 약한 참조가 nil이 되도록 설정합니다. 그리고 약한 참조는 런타임에 nil로 변경될 수 있기 때문에 항상 상수보다 옵셔널 타입의 변수로 선언해야 합니다.

 

약한 참조에서 값이 존재하는 것은 다른 옵셔널 값과 동일한 방법으로 체크할 수 있습니다.

ARC가 약한 참조를 nil로 설정할 때, 옵저퍼 프로퍼티는 호출되지 않습니다.

아래 코드는 위에서 본 Person과 Apartment 클래스와 동일한데, 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") }
}

이전과 동일한 코드로 실행해보도록 하겠습니다.

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로 설정됩니다.

이제 오직 unit4A 변수로 인한 Apartment 인스턴스의 강한 참조만이 남아있습니다. 만약 이 강한 참조를 깬다면 더 이상 Apartment 인스턴스에는 강한 참조가 없습니다.

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

더 이상 강한 참조가 없기 때문에, 인스턴스가 해제됩니다.

가비지 컬렉션을 사용하는 시스템에서, 약한 참조는 때때로 간단한 캐싱 메커니즘을 구현하는데 사용될 수 있는데, 이는 강한 참조가 없는 객체는 오직 메모리에서 가비지 컬렉션을 트리거할 때만 해제되기 때문입니다. 그러나 ARC를 사용하는 Swift에서는 마지막 강한 참조가 제거되자마자 그 값은 해제되기 때문에 이러한 목적에는 적합하지 않습니다.

 

Unowned References

약한 참조와 마찬가지로 미소유 참조는 참조하는 인스턴스를 강하게 붙잡고 있지 않습니다. 하지만, 약한 참조와는 달리 미소유 참조는 다른 인스턴스가 동일하거나 더 긴 생애주기를 가질 때 사용됩니다. 미소유 참조는 프로퍼티나 변수 선언 바로 앞에 unowned 키워드를 사용하여 미소유 참조라는 것을 가리킬 수 있습니다.

 

약한 참조와 달리, 미소유 참조는 항상 값을 가진다고 예상합니다. 그 결과 미소유 참조인 값은 옵셔널로 지정할 필요가 없으며, ARC는 미소유 참조를 절대 nil로 설정하지 않습니다.

미소유 참조는 항상 레퍼런스가 해제되지 않는 인스턴스를 참조한다고 확실할 때 사용합니다. 만약 인스턴스가 해제된 후에 미소유 참조의 값에 접근하려고 시도한다면 런타임 에러가 발생합니다.

바로 아래에서 살펴볼 예제 코드는 Customer와 CreditCard라는 두 클래스를 정의하는데, 은행 고객과 고객이 사용하는 신용카드를 모델링합니다. 두 클래스는 각각 서로의 클래스의 인스턴스를 프로퍼티로 저장합니다. 이 관계는 잠재적으로 강한 참조 순환을 발생시킬 수 있습니다.

 

Customer와 CreditCard간의 관계는 약한 참조 예제에서 살펴본 Apartment와 Person의 관계와는 미묘하게 다릅니다. 데이터 모델에서 고객은 신용카드가 있을 수도 있고 없을 수도 있습니다. 하지만 신용카드는 항상 고객과 연관있습니다. CreditCard 인스턴스는 절대 자신을 참조하는 Customer 없이 존재할 수 없습니다. 이를 표현하기 위해서 Customer 클래스는 옵셔널인 card 프로퍼티를 가지고 있지만, 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 속성은 Int가 아닌 UInt64 타입으로 정의되었는데, 이는 32bit와 64bit 시스템에서 모두 16자리의 숫자를 저장하기에 충분한 크기의 공간을 확보하기 위해서입니다.

다음 코드는 옵셔널 Customer 변수 john을 정의합니다. nil로 초기화됩니다.

var john: Customer?

이제 Customer 인스턴스를 생성한 뒤, 새로운 CreditCard 인스턴스를 생성 및 초기화하고 john의 card 프로퍼티에 할당합니다.

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

이제 두 인스턴스를 연결되었고, 참조는 다음과 같이 구성됩니다.

Customer 인스턴스는 CreditCard에 강한 참조를 가지지만, CreditCard는 Customer 인스턴스에 대해 미소유 참조를 갖습니다. 미소유 customer 참조 때문에 john 변수에 의해서 잡혀있는 강한 참조를 깨버렸을 때, Customer 인스턴스에는 더 이상 강한 참조가 없습니다.

Customer 인스턴스에는 더 이상 강한 참조가 없기 때문에 해제됩니다. 해제가 되고 나면 CreditCard에도 강한 참조가 없으므로 마찬가지로 해제됩니다.

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

위 코드를 실행하면 Customer 인스턴스와 CreditCard 인스턴스가 모두 디이니셜라이저의 메시지를 출력하는 것을 볼 수 있습니다.

 

Unowned Optional References

클래스에 대한 옵셔널 참조에 unowned를 표시할 수 있습니다. ARC 모델 관점에서 미소유 옵셔널 참조와 약한 참조는 동일한 문맥에서 둘 다 사용할 수 있습니다. 차이점은 미소유 옵셔널 참조를 사용할 때, 항상 유효한 객체를 참조하거나 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
    }
}

Department는 학과에서 제공하는 각 수업을 강한 참조로 유지합니다. ARC 모델에서 학과는 각 수업을 소유합니다. Course는 두 개의 미소유 참조를 갖는데, 하나는 학과에 관한 것이고, 다른 하나는 학생을 들어야 하는 다음 수업에 관한 것입니다. 모든 수업은 학과의 일부이므로 department 프로퍼티는 옵셔널이 아닙니다. 그러나 몇몇 수업은 다음에 들어야할 수업이 없을 수도 있으므로 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개의 수업을 생성합니다. intro와 intermediate 수업은 둘다 해당 수업 다음에 들어야하는 수업을 그들의 nextCourse 프로퍼티에 저장합니다. 따라서 이 수업을 완료한 학생이 들어야 하는 수업에 대해 미소유 옵셔널 참조를 유지합니다.

미소유 옵셔널 참조는 이들이 감싸는 클래스 인스턴스를 강한 참조로 붙잡고 있지 않기 때문에 ARC가 그 인스턴스를 해제하는 것을 막지 못합니다. 이는 미소유 옵셔널 레퍼런스가 nil로 설정되어야 한다는 것을 제외하면 단순 미소유 참조와 동일한 방식으로 동작합니다.

 

옵셔널이 아닌 미소유 참조와는 달리, nextCourse가 참조하는 수업이 해제되지 않는다는 것을 보장해주어야 합니다. 예를 들어, department.courses에서 수업을 삭제할 때, 삭제된 수업을 참조하는 다른 수업들에서의 참조들도 제거해주어야 합니다.

 

Unowned References and Implicitly Unwrapped Optional Properties

위에서 살펴본 약한 참조 및 미소유 참조의 예제들은 강한 참조 순환을 막기 위해 필요한 일반적인 두 시나리오를 다루고 있습니다.

 

Person과 Apartment 예제에서는 두 인스턴스가 nil이 될 수 있고, 잠재적으로 강한 순환 참조를 발생시킬 수 있다는 것을 보여줍니다. 이 시나리오의 문제는 약한 참조를 사용하여 해결할 수 있습니다.

 

Customer와 CreditCard 예제에서는 nil이 될 수 있는 한 프로퍼티와 nil이 될 수 없는 다른 프로퍼티가 강한 참조 순환을 발생시킬 수 있다는 상황을 보여줍니다. 이 시나리오의 문제는 미소유 참조로 잘 해결할 수 있습니다.

 

그러나 두 속성 모두 항상 값을 가져야하고, 초기화가 완료될 때 모두 nil이 되어서는 안되는 세 번째 시나리오가 있습니다. 이 시나리오에서는 한 클래스의 미소유 프로퍼티와 다른 클래스의 암시적으로 언래핑한 옵셔널 프로퍼티를 결합하는 것이 유용합니다.

이렇게 하면 초기화가 완료될 때 두 속성 모두(옵셔널 언래핑없이) 직접 액세스될 수 있으며, 참조 순환을 피할 수 있습니다. 이번에는 이러한 관계를 설정하는 방법에 대해서 살펴보겠습니다.

 

아래 예제 코드는 Country와 City라는 두 클래스를 정의하는데, 각 클래스는 다른 클래스의 인스턴스를 프로퍼티로 저장힙니다. 이 데이터 모델에서 모든 나라는 항상 수도를 가지며, 각 도시는 항상 나라에 속해야합니다. 이를 표현하기 위해서 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 인스턴스가 완전히 초기화될 때까지 self를 City 이니셜라이저에 전달할 수 없습니다.

이 문제에 대처하기 위해서 Country의 capitalCity 프로퍼티를 암시적으로 언래핑된 옵셔널 프로퍼티로 선언합니다. 이 속성은 타입 어노테이션 끝에 느낌표를 붙여 표시합니다(City!). 이는 capitalCity 프로퍼티가 다른 옵셔널처럼 nil의 초기값을 가질 수 있다는 것을 의미하지만 언래핑할 필요없이 액세스될 수 있습니다.

 

capitalCity는 디폴트값으로 nil을 가지기 때문에 새로운 Country 인스턴스는 이니셜라이저 내에서 name 프로퍼티만 설정하면 완전히 초기화되었다고 간주됩니다. 따라서 Country 이니셜라이저는 name 프로퍼티가 설정되자마자 self를 참조하고 전달할 수 있습니다. 그러므로 Country 이니셜라이저는 self를 City의 이니셜라이저의 파라미터 중 하나로 전달할 수 있습니다.

 

따라서, Country와 City 인스턴스는 아래 코드처럼 한 문장으로 생성할 수 있으며, 강한 참조 순환도 발생시키지 않습니다. 그리고 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

강한 참조 순환은 클래스 인스턴스의 프로퍼티에 클로저(closure)를 할당하고 클로저의 바디 내에서 그 인스턴스를 참조할 때 발생할 수 있습니다. 클로저의 바디에서 self.someProperty처럼 인스턴스의 프로퍼티에 액세스하거나, self.someMethod()와 같이 인스턴스의 메소드를 클로저가 호출하기 때문에 발생할 수 있습니다. 두 경우 모두 이러한 액세스가 클로저가 self를 캡처하도록 하고 결국 강한 순한 참조를 일으킵니다.

 

클로저가 클래스처럼 레퍼런스 타입이기 때문에 강한 순환 참조가 발생합니다. 프로퍼티에 클로저를 할당할 때 그 클로저는 레퍼런스로 할당됩니다. 결국, 본질적으로는 위에서 본 것처럼 두 강한 참조가 서로를 붙잡고 있는 것입니다. 다만 여기서는 두 클래스 인스턴스 간이 아닌 클래스 인스턴스와 클로저 간에 발생합니다.

 

Swift는 이러한 문제를 해결하기 위해서 클로저 캡처 리스트(closure capture list)라는 솔루션을 제공합니다. 이 솔루션에 대해 알아보기 전에 먼저 이러한 사이클이 어떻게 발생하는지 살펴보도록 하겠습니다.

 

다음 예제 코드는 self를 참조하는 클로저를 사용할 때 강한 참조 순환이 생성되는 모습 보여줍니다. 이 예제 코드는 HTMLElement 클래스를 정의하는데, 이 클래스는 HTML 문서 내에서 각 element를 위한 간단한 모델을 제공합니다.

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

}

위 클래스에서 asHTML이라는 lazy 프로퍼티를 정의합니다. 이 프로퍼티는 name과 text를 HTML string fragment로 결합하는 클로저를 참조합니다. asHTML 프로퍼티의 타입은 () -> String입니다.

기본적으로 asHTML 프로퍼티에는 클로저가 할당되며 이 클로저는 HTML 태그의 문자열 표현을 리턴합니다. 이 태그에는 존재한다면 옵셔널 text 값이 포함되거나 text가 존재하지 않는다면 아무런 텍스트도 없습니다. 예를 들어 paragraph element에서 클로저는 text 프로퍼티가 "some text"거나 nil일 때 "<p>some text</p>"나 "<p />"를 리턴할 것입니다.

 

asHTML 프로퍼티는 인스턴스의 메소드처럼 명명되었고, 메소드처럼 사용됩니다. 그러나 asHTML은 인스턴스 메소드가 아닌 클로저 프로퍼티이기 때문에 asHTML 프로퍼티의 기본값을 커스텀 클로저로 대체할 수 있습니다. 예를 들어 asHTML 프로퍼티는 다음과 같이 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>"

 

아래 코드는 HTMLElement 클래스를 사용해 새로운 인스턴스를 생성하고 출력합니다.

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

 

위에서 작성된 HTMLElement 클래스는 HTMLElement 인스턴스와 클로저 간의 강한 순한 참조를 발생시키고 클로저는 인스턴스의 디폴트 asHTML 값을 사용합니다. 아래 그림은 순환 참조가 어떻게 구성되어 있는지 보여줍니다.

인스턴스의 asHTML 프로퍼티는 클로저를 강한 참조로 참조합니다. 그러나 클로저는 바디에서 self를 참조하고 있기 때문에 클로저는 self를 캡처하고, 이는 HTMLElement 인스턴스를 뒤에서 강한 참조로 붙잡고 있다는 것을 의미합니다. ㅏ따라서 이 둘 간에 강한 참조 순환이 발생합니다.

 

만약 paragraph 변수를 nil롤 설정해서 HTMLElement 인스턴스의 강한 참조를 제거하려고 시도하더라도 HTMLElement 인스턴스와 클로저는 해제되지 않습니다.

paragraph = nil

위의 코드를 실행해도 HTMLElement 디이니셜라이저의 메세지는 출력되지 않습니다.

 


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
}

클로저가 만약 파라미터 리스트나 리턴 타입을 지정하지 않는다면, 다음과 같이 작성할 수도 있습니다.

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

 

Weak and Unowned References

클로저와 클로저가 캡처하는 인스턴스가 항상 서로를 참조하고 동시에 해제된다면 미소유 참조로 캡처를 정의합니다.

반대로 캡처된 참조가 미래의 어느 시점에 nil이 될 수 있다면 약한 참조로 캡처를 정의합니ㅛ다. 약한 참조는 항상 옵셔널 타입이고, 클로저가 참조하는 인스턴스가 해제될 때 자동으로 nil이 됩니다.

 

미소유 참조는 위에서 살펴본 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 클로저 내에서 캡처리스트만 추가되었습니다. 여기서 캡처 리스트는 [unowned self]이며 이는 self를 강한 참조가 아닌 미소유 참조로 캡처한다는 것을 의미합니다.

 

전과 동일하게 다음과 같이 HTMLElement 인스턴스를 생성하고 출력할 수 있습니다.

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

위 코드에서 참조된 상황을 나타내면 다음과 같습니다.

강한 순환 참조 문제가 해결되었기 때문에, 아래 코드처럼 paragraph 변수에 nil을 설정하면 HTMLElement 인스턴스는 해제되고 디이니셜라이저의 메세지가 출력될 것입니다.

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

 

'프로그래밍 > Swift' 카테고리의 다른 글

[Swift] Memory Safety  (0) 2022.03.11
[Swift] Opaque Types  (0) 2022.03.09
[Swift] Error Handling  (0) 2022.03.08
[Swift] Generics (제네릭)  (0) 2022.03.07
[Swift] Protocols (2)  (0) 2022.02.05

댓글