본문 바로가기

Swift

Automatic Reference Counting (ARC)

728x90

오랜만에 돌아왔습니다. 무엇으로? ARC로.

자다가도 누가 ARC가 뭐야?라고 물어보면 답해야 하는 것이 iOS 개발자의 숙명 .. 

 

공식문서를 읽어보면서(= 번역하면서) ARC를 제대로 이해해보자!!!!!!


서론

Swift는 ARC(Automatic Reference Counting)를 사용하여 앱의 메모리 사용량을 추적하고 관리한다. 대부분의 경우, Swift에서 자동으로 메모리를 관리하기 때문에 이에 직접적으로 생각할 필요가 없다.

ARC는 클래스 인스턴스가 더 이상 필요하지 않게 되면 메모리에서 인스턴스를 자동으로 해제한다.

 

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

 

참조 카운트(Reference Count)는 클래스의 인스턴스에서만 적용된다.

구조체와 열거형은 참조 유형이 아닌 값 유형이며 저장되거나 참조로 전달 되지 않는다. (= 고로 ARC에 대해 고려할 필요가 없다.)

 


How ARC Works

클래스의 새로운 인스턴스를 만들 때마다 ARC는 메모리를 할당하여 해당 인스턴스에 대한 정보를 저장한다. 이 메모리에는 인스턴스 유형에 대한 정보와 해당 인스턴스와 관련된 속성 값이 저장된다.

 

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

 

그러나, ARC가 아직 사용 중인 인스턴스의 할당을 해제하면 인스턴스의 속성에 접근하거나 해당 인스턴스의 메서드를 호출할 수 없게 된다. (인스턴스 할당이 해제되고 나서 해당 인스턴스에 접근을 하게 되면 앱 크래쉬가 발생할 가능성이 높다.)

 

인스턴스가 필요한 동안 인스턴스가 사라지지 않도록 ARC는 현재 각 클래스의 인스턴스를 참조하고 있는 속성(상수 및 변수)의 수를 추적한다. ARC는 인스턴스가 적어도 1개의 유효한 참조가 존재하는 한 메모리에서 인스턴스를 해제하지 않는다.

 

이를 위해 클래스 인스턴스를 속성(상수 또는 변수)에 할당할 때마다 해당 인스턴스를 강력하게 참조한다.(= strong) 참조는 해당 인스턴스를 확실하게 유지하며 강한 참조(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")

 

Person 클래스의 초기화가 호출되는 시점에 "John Appleseed" 메세지가 출력된다.

이렇게 초기화가 수행되었다는 것을 확인할 수 있다.

 

새로운 Person 인스턴스가 reference1 변수에 할당 되었기 때문에 새로운 Person 인스턴스에 대한 강한 참조가 존재한다. (기본적으로 강한 참조로 할당)

강한 참조가 하나 이상 있기 때문에 ARC는 해당 Person이 메모리에 보관되고, 메모리 해제가 되지 않도록 한다.

 

아래의 코드와 같이 동일한 Person 인스턴스를 나머지 두 개의 변수에 할당하면 해당 인스턴스에 대한 강한 참조가 두 개 더 설정된다.

reference2 = reference1
reference3 = reference1

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

 

reference1 = nil
reference2 = nil

이 때, 위의 코드와 같이 reference1과 reference2에 nil을 할당하면 강한 참조 두 개가 사라지게 된다.

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

 

그리고 마지막으로 reference3에도 nil을 할당하면 Person 인스턴스의 모든 강한 참조가 사라지면서 ARC에 의해 메모리에서 해제된다. (= deallocated 된다.)

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

 

 


Strong Reference Cycles Between Class Instances

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

 

그러나 클래스 인스턴스가 강력한 참조가 0인 지점에 도달하지 못하는 상황이 있다.

두 클래스 인스턴스가 서로 강한 참조를 유지하여 각 인스턴스가 다른 인스턴스를 active한 상태로 유지하는 경우에 발생한다. 이를 강한 순환 참조라고 한다.

 

클래스 간의 관계 중에서 일부를 강한 참조가 아니라, 약한 참조 또는 미소유 참조로 정의하여 강한 순환 참조를 해결한다. (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?

Person? 타입의 john 변수와 Apartment? 타입의 unit4A 변수를 정의한다. 두 변수는 모두 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 인스턴스가 dellocated 되는 것을 방지하여 앱에서 메모리 누수를 유발한다.

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

 

 


Resolving Strong Reference Cycles Between Class Instances

Swift에서는 약한 참조와 미소유 참조를 사용하여 강한 순환 참조를 생성하지 않고 서로를 참조할 수 있다.

 

약한 참조는 다른 인스턴스가 먼저 deallocated 될 수 있는 경우 사용한다.

위의 Apartment 예시에서 세임자는 없을 수 있는 시점이 있기 때문에 약한 참조를 사용하는 것이 적절하다. 반대로 미소유 참조는 다른 인스턴스의 수명이 같거나 긴 경우에 사용한다.

 

Weak Reference

약한 참조는 참조하는 인스턴스를 강하게 유지하지 않는 참조이므로 ARC가 참조된 인스턴스를 정리하는 것을 중단하지 않는다.

즉, 참조가 강한 순환 참조의 일부가 되는 것을 방지한다. 속성 또는 변수 선언 앞에 weak 키워드를 배치하여 약한 참조를 나타낸다. 

 

약한 참조는 참조하는 인스턴스를 강하게 유지한다.

따라서 약한 참조가 참조하는 동안 해당 인스턴스가 deallocated 될 수 있고 이 때 ARC는 자동으로 nil로 설정한다. 

그리고 약한 참조는 값이 nil로 변경이 가능해야 하므로 상수가 아닌 옵셔널 변수로 선언해야 한다. (그래서 weak var로 선언)

다른 옵셔널 변수와 마찬가지로 약한 참조에 값이 있는지 확인할 수 있다.

 

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

위에서 사용한 Person과 Apartment 예제와 동일하며 Apartment의 tenant가 weak으로 선언되어 있다. 

 

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 변수이다.

이 강한 참조가 nil로 할당하여 해제되면 Apartment 인스턴스에는 더 이상 강한 참조가 없다. 그렇기 때문에 메모리에서 해제된다. 

 

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

 

 


Unowned References

약한 참조처럼 미소유 참조는 참조하는 인스턴스를 강하게 유지하지 않는다.

그러나 약한 참조와 다르게 미소유 참조는 다른 인스턴스의 수명이 같거나 긴 경우에 사용된다. 속성 또는 변수 선언 앞에 unowned 키워드를 작성하여 나타낸다. 

 

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

 

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

 

아래의 예제를 통해서 살펴보자.

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 두 가지 클래스를 정의한 것이다.

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

 

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 = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"

미소유 참조로 인해 john 변수가 보유하고 있는 강한 참조를 해제하면 Customer 인스턴스에 대한 강한 참조는 더 이상 존재하지 않는다.

그러므로 위의 코드처럼 john이 nil로 설정된 후 모든 deinit 메세지가 출력되는 것을 확인할 수 있다. 

 

Customer 인스턴스에 대한 강한 참조가 더 이상 없기 때문에 deallocated 된다.

이 경우 CreaditCard에 대한 강한 참조가 더 이상 없으며 해당 인스턴스의 할당도 해제된다.

 

위의 예시는 미소유 참조를 안전하게 사용하는 방법을 보여준 것이다.

wift는 성능상의 이유로 런타임 안전성 검사를 비활성화해야 하는 경우에 대한 안전하지 않은 미소유 참조를 제공한다. (= 런타임 안정성 검사를 하지 않기 때문에 미소유 참조에 대해서 책임은 개발자에게 있다.) 안전하지 않는 동작이 있을 때, 개발자는 안전을 위해 코드를 확인할 책임을 갖고 있다.

 

참조하는 인스턴스가 해제될 때 안전하지 않은 미소유 참조로서 접근하고자 한다면 unowned(unsafe)를 작성하여 안전하지 않은 미소유 참조를 나타낼 수 있다. 그러면 불안전한 작동 방법으로 인스턴스가 사용되던 메모리 위치의 접근 시도를 할 수 있다.
(= 메모리에 해당 인스턴스가 있다고 확신한 상태로 메모리 위치의 접근 시도를 하게 되어 만약 없을 경우 런타임 에러가 발생할 수 있다.)

 

Unowned Optional Reference

클래스 옵셔널 변수에도 미소유 참조로 표시할 수 있다.

약한 참조와 미소유 참조 객체 모두 옵셔널 타입으로 선언할 수 있다.

약한 참조 객체는 참조가 사라지면 nil이 할당되지만 미소유 참조 객체는 항상 값을 갖고 있기 때문에 nil이 할당되지 않는다.

그래서 미소유 참조 객체를 옵셔널 타입으로 선언할 때는 이 객체가 항상 유효하고(값을 갖고 있고) 그렇지 않을 경우에는 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에서 제공하는 Course를 보여주는 예시이다.

 

Department는 각 Course에 대한 강한 참조를 유지한다.

Course에는 두 개의 미소유 참조가 있다. 하나는 Department에 대한 참조이고 다른 하는 다음 Course에 대한 참조이다. 

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를 생성한다.

위 코드의 상황을 그림으로 나타내면 아래와 같다.

이 때, 미소유 옵셔널 참조는 해당 참조가 래핑되는 클래스의 인스턴스를 강하게 유지하지 않기 때문에 ARC가 인스턴스 할당을 취소한다.

미소유 옵셔널 참조가 0일 수 있다는 점을 제외하고 기존의 미소유 참조가 ARC에서 수행하는 것과 동일하게 동작한다.

 

옵셔널이 아닌 미소유 참조와 마찬가지로 nextCourse가 항상 deallocated 되지 않은 course를 참조하도록 해야한다.

예를 들어서 department.courses의 course를 지운다면 지운 course를 참조하는 다른 course도 지워야 한다.

 

 

Unowned References and Implicitly Unwrapped Optional Property

위의 약한 참조 및 미소유 참조의 예시에서는 강한 순환 참조를 끊어야 하는 두 가지 일반적인 시나리오를 다뤘다.

Person 및 Apartment 예시에서는 모두 nil이 허용되는 두 가지 속성이 강한 순환 참조를 발생시킬 수 있는 가능성을 보여준다. 이 때의 경우는 약한 참조로 해결하는 것이 가장 바람직하다.

 

Customer와 CreditCard 예시에서는 nil이 허용되는 하나의 속성과 nil이 될 수 없는 다른 속성이 강한 순환 참조를 발생시킬 수 있는 상황을 보여준다. 이 시나리오는 미소유 참조를 사용해서 해결하는 것이 가장 좋다.

 

그러나 마지막 상황에서는 두 속성 모두 항상 값이 있어야 하며 초기화가 완료된 후에는 모두 nil이 되어서는 안된다. 

이 상황에서는 한 클래스의 미소유 속성을 다른 클래스의 암시적으로 래핑되지 않은 옵셔널 속성과 결합하여 사용하는 것이 유용하다.

이렇게 하면 초기화가 완료된 이후 두 속성에 접근할 수 있고 순환 참조 문제도 피할 수 있다. 

 

 

Strong Reference Cycles for Closures

강한 순환 참조는 클래스 인스턴스의 프로퍼티에 클로저를 할당하고 클로저의 바디 내에서 그 인스턴스를 참조할 때 발생할 수 있다.

클로저의 바디에서 self.someProperty처럼 인스턴스의 프로퍼티에 접근하거나 self.someMethod()와 같이 인스턴스의 메서드를 클로저가 호출하기 때문에 발생할 수 있다.

두 경우 모두 이러한 접근이 클로저가 self를 캡처하도록 하고 결국 강한 순환 참조를 일으키게 된다.

 

클로저가 클래스처럼 레퍼런스 타입이기 때문에 강한 순환 참조가 발생한다.

프로퍼티에 클로저를 할당할 때 그 클로저는 래퍼런스로 할당이 된다. 결국, 본질적으로는 위에서 보았던 강한 순환 참조의 문제가 발생한 것과 같이 두 강한 참조가 서로를 붙잡고 있는 것이다.

다만, 여기서는 두 클래스 인스턴스 간이 아닌 클래스 인스턴스와 클로저 간에 생기는 문제이다.

 

Swift는 이러한 문제를 해결하기 위해서 클로저 캡처 리스트라는 해결 방법을 제공한다.

이 해결 방법에 대해서 알아보기 전에 먼저 이러한 사이클이 어떻게 발생하는지 알아보자.

 

아래의 코드는 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이라는 lasy 속성을 정의한다.

이 속성은 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"

 


ㄲ .. 끝 .. !

내 머리 속에 과연 잘 들어갔는가 .. ?!?!? 이해해. 김소깡.