Swift/Swift 가이드

[Swift] 공식 문서 - Automatic Reference Counting(ARC)

유정주 2022. 8. 1. 20:19
반응형

안녕하세요. 개발하는 정주입니다.

 

오늘은 "Automatic Reference Counting"를 정리해보겠습니다.

 

* 완벽한 번역이 아닌 내용을 한 줄 한 줄 읽는 것에 의의를 두었습니다.

* 파파고의 힘을 빌려 번역했으며 잘못된 번역이 있다면 댓글로 알려주세요.

 

새로 배운 점

  • 참조 카운트는 클래스의 인스턴스에서만 적용된다.
  • ARC는 메모리 해제 뿐만 아니라 메모리 할당, 추적의 역할도 한다.
  • 약한 참조는 다른 인스턴스의 수명이 더 짧을 때 사용해야 한다.
  • 미소유 참조에 대한 개념
  • 미소유 참조는 다른 인스턴스의 수명이 같거나 더 길 때 사용해야 한다.
  • 모두 nil이 허용되면 weak를 이용해 강한 순환 참조를 방지한다.
  • nil이 허용되는 하나의 속성과 nil이 될 수 없는 다른 속성일 때는 unowned를 이용해 강한 순환 참조를 방지한다.
  • 캡처 리스트에 대한 전반적인 내용

 

서론

Swift는 ARC(Automatic Reference Counting)를 사용하여 앱의 메모리 사용량을 추적하고 관리합니다. 대부분의 경우는 Swift에서 자동으로 메모리를 관리하므로 이에 관해 직접 생각할 필요가 없습니다. ARC는 클래스 인스턴스가 더 이상 필요하지 않게 되면 클래스 인스턴스에서 사용되는 메모리를 자동으로 해제(free)합니다.

 

단, ARC에서는 메모리를 관리하기 위해 코드 부분 간의 관계에 대한 자세한 정보가 필요한 경우가 있습니다. 이 장에서는 이러한 상황을 설명하고 ARC가 앱의 모든 메모리를 관리할 수 있도록 하는 방법을 설명합니다.

 

참조 카운트는 클래스의 인스턴스에서만 적용됩니다. 구조체와 열거형은 참조 유형이 아닌 값 유형이며, 저장되거나 참조로 전달되지 않습니다.

 

How ARC Works

클래스의 새 인스턴스를 만들 때마다 ARC는 메모리 청크(chunk: Heap 메모리에 할당 받는 영역과 header를 포함한 영역)를 할당하여 해당 인스턴스에 대한 정보를 저장합니다. 이 메모리에는 인스턴스 유형에 대한 정보와 해당 인스턴스와 관련된 저장된 속성 값이 저장됩니다.

 

또한 인스턴스가 더 이상 필요하지 않게 되면 ARC는 해당 인스턴스에서 사요외는 메모리를 해방하여 메모리를 다른 용도로 사용할 수 있도록 합니다. 이렇게 하면 클래스 인스턴스가 더 이상 필요하지 않을 때 메모리 공간을 차지하지 않습니다.

 

그러나 ARC가 아직 사용 중인 인스턴스의 할당을 해제하면 해당 인스턴스의 속성에 액세스하거나 해당 인스턴스의 메서드를 호출할 수 없게 됩니다. 실제로 인스턴스에 액세스하려고 하면 앱이 크래쉬할 가능성이 높습니다.

 

인스턴스가 필요한 동안 인스턴스가 사라지지 않도록 ARC는 현재 각 클래스 인스턴스를 참조하고 있는 속성, 상수 및 변수의 수를 추적합니다. ARC는 인스턴스에 대한 적어도 1개의 활성 참조(active reference)가 존재하는 한 인스턴스의 할등을 해제하지 않습니다.

 

이를 위해 클래스 인스턴스를 속성, 상수 또는 변수에 할당할 때마다 해당 속성, 상수 또는 변수가 인스턴스를 강력하게 참조합니다. 참조는 해당 인스턴스를 확실하게 유지하며 강한 참조(strong reference)가 남아 있는 한 할당 해제를 허용하지 않으므로 "강한" 참조(strong reference)라고 합니다.

 

ARC in Action 

다음은 ARC의 작동 예를 보여줍니다. 

Person에 name이라는 stored constant property를 정의합니다.

 

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

Person 클래스에는 인스턴스의 initializer가 설정되어 있습니다. init에는 name 속성 및 초기화 메시지를 출력하고 deinit에는 할당 해제 시 메시지를 출력합니다.

 

다음 코드는 새로운 인스턴스에 대한 여러 참조를 설정하는 데 사용되는 Person? 변수를 정의합니다.

optional type은 기본적으로 nil로 초기화가 되기 때문에 이 변수들은 아무것도 참조하지 않습니다.

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

 

위 변수들은 아래 코드처럼 새로운 Person 인스턴스를 생성할 수 있습니다.

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

Person class의 initailizer가 호출되는 시점에 "John ~~~" 메시지가 출력됩니다.

이렇게 초기화가 수행되었다는 것을 알 수 있습니다.

 

새로운 Person 인스턴스가 reference1 변수에 할당되었기 때문에 reference1에서 새로운 Person 인스턴스에 대한 강한 참조가 존재합니다. 강한 참조가 하나 이상 있으므로 ARC는 해당 Person이 메모리에 보관되고 메모리 해제가 되지 않도록 합니다.

 

동일한 Person 인스턴스를 나머지 두 개의 변수에 할당하면 해당 인스턴스에 대한 강한 참조가 두 개 더 설정됩니다.

reference2 = reference1
reference3 = reference1

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

 

reference1 = nil
reference2 = nil

reference1과 reference2에 nil을 할당하면 강한 참조 두 개가 깨집니다.

Person 인스턴스에는 한 개의 강한 참조가 남아있기 때문에 메모리 해제가 되지 않습니다.

 

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

reference3에도 nil을 할당하면 Person 인스턴스의 모든 강한 참조가 깨지면서 ARC에 의해 deallocated가 됩니다.

 

Strong Reference Cycles Between Class Instances

위의 예처럼 ARC는 사용자가 만든 Person 인스턴스에 대한 참조 수를 추적하고 더 이상 필요하지 않을 때 Person 인스턴스를 해제시킬 수 있습니다.

 

그러나 클래스 인스턴스가 강력한 참조가 0인 지점에 도달하지 못하는 코드가 있습니다. 두 클래스 인스턴스가 서로 강한 참조를 유지하여 각 인스턴스가 다른 인스턴스를 active 상태로 유지하는 경우에 발생합니다. 이를 강한 순환 참조(strong refernce cycle)이라고 합니다.

 

클래스 간의 관계 중 일부를 강한(strong) 참조가 아니라 약한(weak) 참조 또는 미소유(unowned) 참조로 정의하여 강한 순환 참조를 해결합니다. 강한 순환 참조가 왜 발생하는지 아는 것이 중요합니다.

 

다음은 강한 순환 참조가 생성되는 예시입니다. 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 속성을 가집니다.

모든 Apartment 인스턴스는 unit 속성을 가지고 nil로 초기화된 Person?형인 tenant 속성을 가집니다.

두 클래스 모두 deinit에 메시지를 출력합니다. 이렇게 하면 Person 및 Apartment 인스턴스가 예상대로 deallocated 되는지 확인할 수 있습니다.

 

var john: Person?
var unit4A: Apartment?

john Person?형 변수와 unit4A라는 Aparment?형 변수를 정의합니다. 두 변수는 nil로 초기화되어 있습니다.

 

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

john과 unit4A 각각 새로운 인스턴스를 생성하였습니다.

 

 

john 변수는 Person 인스턴스를 강한 참조하고 있고, unit4A 변수는 Apartment 인스턴스를 강한 참조하고 있습니다.

 

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

john의 apartment에는 unit4A를, unit4A의 tenant에는 john을 할당합니다.

 

 

이 두 인스턴스를 연결하면 서로 간에 강한 순환 참조가 생성됩니다. 이제 Person 인스턴스는 Apartment 인스턴스에 대한 강한 참조를 가지고 Apartment 인스턴스는 Person에 대한 강한 참조를 가집니다.

따라서 john 및 unit4A 변수에 의해 유지되는 강한 참조를 끊어도 참조 카운트는 0으로 떨어지지 않으며 인스턴스는 ARC에 의해 deallocated 되지 않습니다.

 

john = nil
unit4A = nil

이 두 변수를 nil로 설정할 때 두 개의 deinit 모두 호출되지 않았습니다.

 

강한 순환 참조는 Person 및 Apartment 인스턴스가 deallocated 되는 것을 방지하여 앱에서 memory leak을 유발합니다.

Person 인스턴스와 Apartment 인스턴스 사이의 강한 참조는 그대로 유지되며 끊을 수 없습니다.

 

Resolving Strong Reference Cycles Between Class Instances

Swift에서는 약한(weak) 참조와 미소유(unowned) 참조를 사용하면 강한 순환 참조를 생성하지 않고 서로를 참조할 수 있습니다.

 

약한 참조는 다른 인스턴스가 먼저 deallocted될 수 있는 경우 사용합니다. 위의 Apartment 예시에서 세입자는 없을 수 있는 시점이 있기 때문에 약한 참조를 사용하는 것이 적절합니다.

반대로 미소유 참조는 다른 인스턴스의 수명이 같거나 긴 경우에 사용합니다.

 

Weak References

약한 참조는 참조하는 인스턴스를 강하게 유지하지 않는 참조이므로 ARC가 참조된 인스턴스를 정리하는 것을 중단하지 않습니다. 이 동작은 참조가 강한 순환 참조의 일부가 되는 것을 방지합니다. 속성 또는 변수 선언 앞에 weak 키워드를 배치하여 약한 참조를 나타냅니다.

 

약한 참조는 참조하는 인스턴스를 강하게 유지하지 않습니다. 따라서 약한 참조가 참조하는 동안 해당 인스턴스가 deallocted 될 수 있고 이때 ARC는 약한 참조를 nil로 자동 설정합니다. 그리고 약한 참조는 런타임에 값이 nil로 변경이 가능해야 하므로 상수가 아닌 옵셔널 변수로 선언해야 합니다. 다른 옵셔널 변수와 마찬가지로 약한 참조에 값이 있는지 확인할 수 있습니다.

ARC가 약한 참조를 nil로 설정하면 속성 옵저버를 호출하지 않습니다.

 

아래 예제는 위의 Person과 Apartment 예제와 동일하며, Apartment의 tenant가 weak로 선언되어 있습니다.

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
// Prints "John Appleseed is being deinitialized"

john 변수를 nil로 설정하여 john 변수가 보유하는 강한 참조를 끊으면 Person 인스턴스에 대한 강한 참조가 더 이상 존재하지 않습니다. 

Person 인스턴스에 대한 강한 참조가 더 이상 없기 때문에 deallocated되고 tenant 속성이 nil로 초기화 됩니다.

 

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

Apartment 인스턴스에 대한 유일한 강한 참조는 unit4A 변수입니다. 이 강한 참조가 깨지면 Apartment 인스턴스에는 더이상 강한 참조가 없습니다. Apartment 인스턴스에 강한 참조가 없기 때문에 deallocated 됩니다.

ARC를 사용하면 약한 참조는 캐싱 메커니즘에 적합하지 않습니다. 마지막 강한 참조가 제거되는 즉시 값이 deallocated 되기 때문입니다.

 

Unowned References

약한 참조처럼 미소유 참조는 참조하는 인스턴스를 강하게 유지하지 않습니다. 그러나 약한 참조와 달리 미소유 참조는 다른 인스턴스의 수명이 같거나 긴 경우 사용됩니다. 속성 또는 변수 선언 앞에 unowned 키워드를 작성하여 나타냅니다.

 

약한 참조와 달리 미소유 참조는 옵셔널이 필수가 아니며 ARC는 미소유 참조 값을 nil로 설정하지 않습니다.

미소유 참조는 항상 deallocated 되지 않은 인스턴스를 참조하는 경우에만 사용해야 합니다.
인스턴스가 deallocated된 후 미소유 참조 값에 접근하려고 하면 런타임 에러가 발생합니다.

 

다음 예제에서는 은행 고객 Customer과 해당 고객에게 가능한 신용 카드 CreditCard 두 가지 클래스를 정의합니다.

이 두 클래스는 각각 다른 클래스의 인스턴스를 속성으로 저장합니다. 이 관계는 강한 순환 참조를 만들 수 있습니다.

 

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

Customer와 CreditCard의 관계는 Apartment와 Person의 관계와 약간 다릅니다.

이 데이터 모델에서 고객은 신용 카드를 가지고 있을 수도 있고 가지고 있지 않을 수도 있지만 신용 카드는 항상 고객과 연결됩니다. 따라서 강한 순환 참조를 피하기 위해 해당 고객 변수를 미소유 참조로 정의합니다.

 

var john: Customer?

john 변수는 특정 고객에 대한 참조를 저장하는데 사용됩니다. 이 변수는 optional이기 때문에 초기값은 nil입니다.

 

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

Customer 인스턴스를 생성하고 CreditCard 인스턴스를 생성합니다.

 

두 인스턴스를 연결했으므로 참조는 다음과 같습니다.

Customer 인스턴스는 CreditCard 인스턴스에 대한 강한 참조를 갖게 되었고 CreditCard 인스턴스는 Customer 인스턴스에 대한 미소유 참조를 가지게 되었습니다.

미소유 참조로 인해 john 변수가 보유하고 있는 강한 참조를 깨면 Customer 인스턴스에 대한 강한 참조가 더이상 없습니다.

 

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

john이 nil로 설정된 후 모든 deinit 메시지가 출력됨을 보여줍니다.

 

Customer 인스턴스에 대한 강한 참조가 더 이상 없기 때문에 deallocated 됩니다. 이 경우 CreditCard에 대한 강한 참조가 더 이상 없으며 해당 인스턴스의 할당도 해제됩니다.

 

위의 예에서는 미소유 참조를 안전하게 사용하는 방법을 보여줍니다. Swift는 성능상의 이유로 런타임 안전성 검사를 비활성화해야 하는 경우에 대한 안전하지 않은 미소유 참조를 제공합니다. 안전하지 않은 동작이 있을 때, 개발자는 안전을 위해 코드를 확인할 책임을 갖고 있습니다.

당신은 참조하는 인스턴스가 해제될 때, 안전하지 않은 미소유 참조로서 접근하고자 한다면 unowned(unsafe)를 작성하여 안전하지 않은 미소유 참조를 나타낼 수 있습니다. 당신의 프로그램은 불안전한 작동방법으로 인스턴스가 사용되던 메모리 위치의 접근 시도를 할 것입니다.

 

Unowned Optional References

클래스 옵셔널 변수에도 미소유 참조로 표시할 수 있습니다. 약한 참조와 미소유 참조 객체 모두 옵셔널 타입으로 선언할 수 있습니다. 약한 참조 객체는 참조가 사라지면 nil이 할당되지만 미소유 참조 객체는 항상 값을 가지고 있기 때문에 nil이 할당되지 않습니다. 그래서 미소유 참조 객체를 옵셔널 타입으로 선언할 때는 이 객체가  항상 유효하고(값을 가지고 있고), 그렇지 않을 경우 nil이 할당됨을 보장해야 합니다.

 

다음은 학교의 특정 Department에서 제공하는 Course를 보여주는 예시입니다.

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는 각 Course에 대한 강한 참조를 유지합니다. Course에는 두 개의 미소유 참조가 있습니다. 하나는 Department에 대한 참조이고 다른 하나는 다음 Cousrse에 대한 참조입니다. Course는 반드시 Department의 한 부분이 되어야 하므로 department 속성은 옵셔널 타입이 아니지만, nextCourse는 존재하지 않을 수 있으므로 옵셔널 타입으로 선언합니다. 그리고 옵셔널 타입의 미소유 참조 객체는 nil이 할당되지 않기 때문에 생성자에서 직접 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]

위 코드는 하나의 Department와 세 개의 Course를 생성합니다. intro와 intermediate는 다음 코스가 지정이 됐습니다. 

 

미소유 옵셔널 참조는 해당 참조가 래핑되는 클래스의 인스턴스를 강하게 유지하지 않으므로 ARC가 인스턴스 할당을 취소하는 것을 막지 않습니다. 미소유 옵셔널 참조가 0일 수 있다는 점을 제외하고 미소유 참조가 ARC에서 수행하는 것과 동일하게 동작합니다.

 

옵셔널이 아닌 미소유 참조와 마찬가지로 nextCourse가 항상 deallocated 되지 않은 course를 참조하도록 해야합니다. 예를 들어, department.courses의 course를 지운다면, 지운 course를 참조하는 다른 course도 지워야 합니다.

 

옵셔널 값의 기본 유형은 Swift 표준 라이브러리의 열거형인 Optional입니다. 그러나 옵셔널에서는 예외적으로 값 유형(value type)을 미소유로 표시할 수 있습니다.
클래스를 래핑하는 옵셔널은 참조 카운트를 사용하지 않으므로 강한 참조를 유지할 필요가 없습니다.

 

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의 initializer는 Country를 인자로 전달받아 설정합니다.

 

City의 initializer는 국가의 initializer 내에서 호출됩니다. 그러나 Country 인스턴스가 완전히 초기화될 때까지 국가에 대한 생성자는 City 생성자에게 자신을 전달할 수 없습니다.

 

이것에 대처하기 위해 Country의 CapitalCity 속성을 강제 언래핑 옵셔널 타입으로 선언합니다. 즉, capitalCity 속성의 기본값은 nil이지만 값을 언래핑할 필요 없이 접근할 수 있습니다.

 

capitalCity에는 기본값이 nil이므로 Country 인스턴스가 생성자 내에서 name 속성을 설정하는 즉시 새 Country 인스턴스가 완전히 initialize된 것으로 간주됩니다. 즉, 국가 생성자는 name 속성을 설정하는 즉시 암시적 self 속성(implicit self property)을 참조하고 전달할 수 있습니다. 따라서 Country initializer의 매개 변수 중 하나로 자신을 전달할 수 있습니다.

 

이 모든 것을 통해 강한 순환 참조를 생성하지 않고도 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"

위의 예시에서 암묵적으로 언래핑 옵셔널의 사용은 모든 two-phase 클래스 initializer 요구 사항을 충족함을 의미합니다. capitalCity 속성은 초기화가 완료된 후 강한 순환 참조를 피하면서 non-optional 값처럼 사용하고 액세스할 수 있습니다.

 

Strong Reference Cycles for Closures

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

 

클로저가 클래스처럼 레퍼런스 타입이기 때문에 강한 순환 참조가 발생합니다. 프로퍼티에 클로저를 할당할 때 그 클로저는 레퍼런스로 할당됩니다. 결국, 본질적으로는 위에서 본 것처럼 두 강한 참조가 서로를 붙잡고 있는 것입니다. 다만 여기서는 두 클래스 인스턴스 간이 아닌 클래스 인스턴스와 클로저 간에 발생합니다.
 
Swift는 이러한 문제를 해결하기 위해서 클로저 캡처 리스트(closure capture list)라는 솔루션을 제공합니다. 이 솔루션에 대해 알아보기 전에 먼저 이러한 사이클이 어떻게 발생하는지 살펴보도록 하겠습니다.

 

다음 예제 코드는 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")
    }

}

위 클래스에서 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 속성은 다음과 같이 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>"

 

새로운 인스턴스를 생성하고 출력합니다.

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

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

 

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 인스턴스는 해제되고 deinit 메세지가 출력될 것입니다.

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

 

참조

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

 

 

반응형