반응형

[최종 수정일 : 2018.09.11]

원문 : 애플 개발 문서 Swift 4.2 Language Guide - Automatic Reference Counting

자동 참조 개수(Automatic Reference Counting)

Swift는 앱의 메모리 사용을 추적하고 관리하기 위해 자동 참조 개수(Automatic Referecne Counting: ARC)를 사용합니다. 대부분의 경우에, Swift에서 이러한 메모리 관리는 그냥 사용(just works)되고, 메모리 관리에 대해 생각할 필요가 없습니다. ARC는 클래스 인스턴스에 의해 인스턴스가 더 이상 필요없을때, 자동으로 메모리를 해제해줍니다.

하지만, 몇가지의 경우에 ARC는 메모리를 관리하기 위한 코드와 관련있는 추가 정보가 필요합니다. 이 챕터(chapter)는 이러한 상황과 앱의 메모리를 모두 관리하기 위해 어떻게 ARC를 활성화하는지 설명합니다. Objective-C 로 ARC 사용하는 것에 대해 ARC 릴리즈로 전환하기(Transitioning to ARC Release Notes) 설명된것처럼, Swift에서 ARC를 사용하는 것은 매우 간단합니다.

참조 개수(reference counting)는 클래스의 인스턴스에서만 적용합니다. 구조체와 열거형은 참조 타입이 아닌 값 타입이고, 참조로 저장하거나 전달하지 않습니다.

ARC 작동방법(How ARC Works)

클래스의 새로운 인스턴스를 생성할때마다 ARC는 인스턴스에 대한 정보를 저장하기 위해 메모리 영역(chunk)을 할당합니다. 이 메모리는 인스턴스와 연관된 모든 저장 프로퍼티의 값과 함께, 타입의 인스턴스에 대한 정보를 유지합니다.

추가적으로, 인스턴스가 더이상 필요하지 않을때, ARC는 메모리가 다른 목족으로 사용할수 있도록 해당 인스턴스에서 사용되는 메모리를 해제합니다. 이것은 클래스 인스턴스가 더 이상 필요하지 않을때, 메모리에서 공간을 차지하지 않도록 해줍니다.

하지만, ARC가 아직 사용하고 있는 인스턴스를 메모리에서 해제하는 경우, 인스턴스의 프로퍼티에 접근하거나 인스턴스의 메소드를 호출하는 것은 더 이상 불가능합니다. 실제로(indeed), 그 인스턴스에 접근하려고 하면, 앱은 크래쉬(crash) 될것입니다.

인스턴스가 필요한 동안에는 사라지지 않도록 해야하며, ARC는 각 클래스 인스턴스를 현재 참조하고 있는 많은 프로퍼티, 상수, 변수들을 추적합니다. ARC는 인스턴스에 대한 활성화된 참조가 최소한 1개라도 있는한, 인스턴스의 메모리를 해제하지 않을 것입니다.

이를 가능하도록 하기 위해, 프로퍼티, 상수나 변수에 클래스 인스턴스를 할당할때마다, 그 프로퍼티, 상수, 변수는 인스턴스에 강한 참조(strong reference)가 되도록 합니다. 인스턴스를 확실히 잡고 있기 때문에, 이러한 참조를 강한(strong) 참조라 하고, 강한 잠조가 남아있는 한, 메모리를 해제하는 것을 허용하지 않습니다.

ARC 동작(ARC in Action)

다음은 ARC(Automatic Reference Counting)의 동작 방법에 대한 예제입니다. 이 예제는 저장 상수 프로퍼티 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 프로퍼티를 설정하고 초기화가 진행중임을 나타내는 메시지를 출력하는 초기화(initializer)를 가집니다. Person 클래스는 클래스의 인스턴스 메모리를 해제할때 메시지를 출력하는 해제(deinitializer)를 가지고 있습니다.

다음 코드 부분은 나중에 오는 코드 부분에서 새로운 Person인스턴스를 여러번 참조하는데 사용되는, Person?타입의 변수 3개를 정의합니다. 이러한 변수들은 원래 옵셔널 타입(Person이 아닌 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 클래스의 초기화가 호출된 시점에 "존 애플시드가 초기화되었습니다(John Appleseed is being initialized)" 메시지가 출력되는 것을 주의하세요. 이것으로 초기화가 이루어졌음을 확인합니다.

새로운 Person 인스턴스는 reference1변수에 할당 되었기 때문에, reference1에 새로운 Person인스턴스에 대한 강한 참조가 있습니다. 이는 적어도 1개의 강한 참조이기 때문에, ARC은 Person이 메모리에 유지하도록 하고 메모리를 해제하지 않습니다.

2개 이상의 변수에 동일한 Person인스턴스를 할당하는 경우, 그 인스턴스는 두개 이상의 강한 참조가 됩니다(established).

reference2 = reference1
reference3 = reference1

이제 하나의 Person인스턴스에 대해 3개의 강한 참조가 있습니다.

2개의 변수에 nil을 할당해서, 이러한 2개의 강한 참조(원래 참조를 포함)가 깨지는 경우, 1개의 강한 참조만 남고, Person인스턴스는 메모리에서 해제되지 않습니다.

reference1 = nil
reference2 = nil

ARC는 Person인스턴스를 더 이상 사용하지 않는다는 것을 명확한 시점에, 3번째 그리고 마지막 강한 참조가 깨질때까지는, Person인스턴스의 메모리를 해제하지 않습니다.

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

클래스 인스턴스간에 강한 순환참조(Strong Reference Cycles Between Class Instances)

위 예제에서, ARC는 새로 생성한 Person인스턴스의 참조 개수를 추적하는것이 가능하고 Person 인스턴스가 더 이상 필요가 없을때, 메모리에서 해제 하는 것이 가능합니다.

하지만, 클래스의 인스턴스가 결코(never) 강한 참조가 없는 지점에에 도달하지 않도록 코드를 작성하는 것이 가능합니다. 이는 2개의 클래스 인스턴스가 각각 서로를 강한 참조로로 잡고 있을때 발생할 수 있으며, 각 인스턴스는 다른 인스턴스를 계속 유지시켜줍니다. 이를 강한 순환 참조(strong reference cycle)라고 합니다.

클래스간에 강한 참조(strong reference) 대신에 weak와 unowned참조로 관계를 정의 함으로써 강한 순환 참조를 해결합니다. 이러한 과정은 클래스 인스턴스간에 강한 참조 해결하기(Resolving Strong Reference Cycles Between Class Instances)에 설명되어 있습니다. 하지만, 강한 순환 참조를 해결하는 방법을 배우기 전에, 이러한 순환이 발생하는 이유에 대해 이해하고 있는 것이 유용합니다.

다음은 우연히 강한 순환 참조가 만들어 질수 있는 방법에 대한 예제입니다. 이 예제는 아파트(apartments)와 거주자(residents)에 대해 모델링을 한, 2개의 클래스 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인스턴스는 언제나 String타입의 name프로퍼티와 nil로 초기화한 옵셔널 apartment프로퍼티를 가지고 있습니다. apartment프로퍼티는 사람은 언제나 아파트를 가지지 않을 수도 있기 때문에, 옵셔널 입니다.

비슷하게, Apartment인스턴스는 언제나 String타입의 unit프로퍼티와 nil로 초기화한 옵셔널 tenant프로퍼티를 가지고 있습니다. tenant프로퍼티는 아프타가 항상 거주자가 있는 것이 아니기 때문에, 옵셔널입니다.

이러한 클래스 모두 해당 클래스의 인스턴스가 해제(deinitialized)된다는 사실을 출력하는 해제를 정의합니다. 이는 Person와 Apartment의 인스턴스가 예상대로 해제되는지 확인할 수 있습니다.

다음 코드 조각은 아래 특정 Apartment와 Person인스턴스를 설정하는 옵셔널 타입의 변수 2개 john과 unit4A를 정의합니다. 이러한 변수들 모두 옵셔널이기에, 초기 값은 nil입니다.

var john: Person?
var unit4A: Apartment?

이제 특정 Person인스턴스와 Apartment인스턴스를 생성할수 있고 새로운 인스턴스들을 john과 unit4A 변수에 할당 할 수 있습니다.

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

다음은 2개의 인스턴스를 생성하고 할당한 후에 강한 참조가 어떻게 보여지는지 입니다. john변수는 이제 새로운 Person인스턴스에 대해 강한 참조를 가지고, unit4A변수는 새로운 Apartment인스턴스에 강한 참조를 가집니다.

이제 2개의 인스턴스를 함께 연결할 수 있기에, person은 apartment를 가지고, apartment는 tenant를 가집니다. 옵셔널 변수 john과 unit4A 안쪽에 저장된 인스턴스를 언래핑(unwrap)하고 사용하기 위해 느낌표(!) 표시 하는 것을 주의하며, 해당 인스턴스의 프로퍼티에 설정할 수 있습니다.

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

다음은 2개의 인스턴스를 함게 연결한 후에 어떻게 강한 참조가 되는지 보여줍니다.

불행하게도, 이러한 2개의 인스턴스들은 서로간에 강한 순환 참조로 연결됩니다.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참조와 unowned참조.

Weak와 unowned참조는 순환 참조에서 한 인스턴스가 다른 인스턴스를 강하게 유지하지 않고, 참조할 수 있도록 합니다. 인스턴스는 강한 순환 참조를 만들지 않고 서로를 참조할 수 있습니다.

다른 인스턴스의 생명주기(lifetime)가 짧을때 weak 참조를 사용합니다. - 이는, 다른 인스턴스를 먼저 해제할 수 있을때입니다. 위의 Apartment 예제에서, 생명주기의 어느 시점에 apartmenttenant가 없는 경우에 어울리고, 이 경우에, weak 참조는 순환 참조를 깨는 적절한 방법입니다. 대조적으로, unowned 참조는 다른 인스턴스가 동일한 생명주기를 가지거나 더 길게 유지될때 사용합니다.

약한 참조(Weak References)

약한 참조(weak reference)는 참조하는 인스턴스를 강하게 유지하지 않는 참조이고, ARC가 참조된 인스턴스를 처리(disposing)하는 것을 멈추지 않습니다. 이 동작은 참조가 강한 순환 참조가 되는것을 막습니다. 프로퍼티나 변수 선언 앞에 weak키워드를 붙여서 약한 참조(weak reference)를 가리킵니다.

약한 참조는 인스턴스 참조를 강하게 유지하지 못하기 때문에, 약한 참조로 참조하는 동안에는, 인스턴스가 메모리에서 해제 되는 것이 가능합니다. 따라서, 참조하는 인스턴스가 메모리에서 해제될때, ARC는 자동으로 약한 참조를 nil로 설정합니다. 그리고, 약한 참조는 실행중에 값이 nil로 변경하도록 허용해야 하기 때문에, 상수보다는 항상 옵셔널 타입의 변수로 선언됩니다.

약한 참조에서 옵셔널 값과 마찬가지로, 값이 존재하는지 검사할 수 있으며, 더 이상 존재하지 않는 유효하지 않은 인스턴스를 참조하지 않을 것입니다.

주의
ARC가 weak참조를 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") }
}

2개의 변수(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인스턴스에 대해 weak 참조를 가지고 있습니다. 이는 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)을 사용하는 시스템에서, weak 포인터는 가비지 컬렉션 메모리 정리가 발생할때에만 강한 참조가 아닌 객체는 해제되기 때문에, 가끔씩 캐쉬 메카니즘 구현을 하기 위해 사용됩니다. 하지만, ARC에서 변수들이 마지막으로 강한 참조가 제거되자마자 메모리에서 해제되기 때문에, 약한 참조는 이러한 목적과 어울리지 않습니다.

소유하지 않는 참조(Unowned References)

약한 참조 처럼, 소유하지 않는 참조(unowned reference)는 인스턴스의 참조를 강하게 유지하지 않습니다. 하지만, 약한 참조와는 다르게, unowned 참조는 다른 인스턴스가 같은 생명주기 또는 더 긴 생명주기를 가질때 사용됩니다. unowned 참조는 프로퍼티나 변수 선언 앞에 unowned 키워드를 위치시켜 가리킵니다.

unowned 참조는 항상 값을 가지고 있는 것으로 에상됩니다. 결과적으로, unowned참조가 옵셔널이 아닌 타입을 사용하여 정의된 것을 의미하며, ARC는 결코 unowned 참조의 값을 nil로 설정하지 않습니다.

주의
참조가 항상(always) 메모리가 해제되지 않은 인스턴스를 참조할때에만, unowned참조를 사용합니다.

인스턴스가 메모리 해제된 후에, unowned 참조의 값을 사용하려는 경우, 런타임 오류가 발생할 것입니다.

다음은 은행 고객과 고객이 사용가능한 신용카드를 모델링한, 2개의 클래스 Customer과 CreditCard를 정의하는 예제입니다. 이러한 2개의 클래스는 각각 다른 클래스의 프로퍼티를 저장합니다. 이 관게는 강한 순환참조를 만들 가능성이 있습니다.

Customer과 CreditCard간의 관계는 위의 약한 참조 예제에서 Apartment와 Person간에 보았던것과 약간 다릅니다. 이 데이터 모델에서, customer은 신용카드를 가지거나 가지지 않을수 있지만, 신용카드는 항상(always) 고객과 관련이 있습니다. CreditCard 인스턴스는 결코 Customer인스턴스보다 더 오래 살아있지 못합니다. 이를 표현하기 위해, Customer클래스는 옵셔널 card 프로퍼티를 가지지만, CreditCard 클래스는 소유하지 않은(unowned)(그리고 옵셔널이 아닌) customer프로퍼티를 가집니다.

게다가(furthermore), 새로운 CreditCard 인스턴스는 사용자정의 CreditCard 초기화에 number 값과 customer인스턴스를 전달 받아야만 생성이 가능합니다. 이렇게 하면 CreditCard 인스턴스가 생성될때, CreditCard인스턴스가 항상 customer인스턴스와 연관되어 있습니다.

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

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프로퍼티의 용량이 32비트와 64비트시스템에서, 16자리 카드 숫자를 저장하기에 충분히 커야하기 때문에, number프로퍼티는 Int보다는 UIt64로 정의됩니다.

다음 코드 부분은 특정 고객에 대한 참조를 저장하는데 사용되는 옵셔널 Customer 변수 john을 정의합니다. 이 변수는 옵셔널이기에, 초기값 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인스턴스에 대해 unowned참조를 가집니다.

unowned customer참조이기 때문에, john변수에 의해 강한 참조가 깨질때, Customer인스턴스에 대한 강한 참조는 더 이상 없습니다.

Customer인스턴스에 대해 더 이상 강한 참조가 없기 때문에, 메모리에서 해제되었습니다. 그리고나 후에, CreditCard 인스턴스에 대해 더 이상 강한 참조가 없고, 마찬가지로 메모리에서 해제됩니다.

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

위의 마지막 코드 부분은 john변수가 nil로 설정된 후에, Customer인스턴스와 CreditCard 인스턴스 모두 "해제됨(deinitialized)" 메시지를 출력하여 헤제되는 것을 보여줍니다.

주의
위 예제는 안전한(safe) unowned 참조를 사용하는 방법을 보여줍니다. Swift는 실시간으로 안전한지 검사하는 것을 비활성화해야 하는 경우에 안전하지 않은(unsafe) unowned 참조를 제공합니다. - 예를들어, 성능상의 이유. 안전하지 않은 동작과 마찬가지로, 코드의 안정성을 확인하는 책임을 져야 합니다.

unowned(unsafe)를 작성해서 안전하지 않은 unowned참조를 가리킵니다. 참조하는 인스턴스가 가 메모리에서 해재된 후에, 안전하지 않은 unowned 참조를 접근하려는 경우에, 프로그램은 사용했던 메모리 위치에 접근하려 할것이며, 이는 안전하지 않는 동작입니다.

소유하지 않는 참조와 암시적으로 언래핑된 옵셔널 프로퍼티(Unowned Reference and Implicitly Unwrapped Optional Properties)

위의 weak와 unowned 참조에 대한 예제는 강한 순환 참조를 깨트릴 필요가 있는 일반적인 2가지 이상의 시나리오를 다룹니다.

Person과 Apartment예제는 2개의 프로퍼티 모두 nil이 허용되는 강한 순환 참조의 원인이 될 가능성이 있는 상황을 보여줍니다. 이 시나리오는 weak참조로 잘 해결 됩니다.

Customer과 CreditCard예제는 하나의 프로퍼티가 nil이 허용되고 다른 프로퍼티는 nil이 될수 없는 강한 순환 참조가 될 가능성이 있는 상황을 보여줍니다. 이 시나리오는 unowned참조로 잘 해결이 됩니다.

하지만, 3번째 시나리오는 두 프로퍼티 모두(both) 항상 값을 가지고 초기화가 한번 완료되면 어떤 프로퍼티도 nil이 될수 없습니다. 이 시나리오 에서, 하나의 클래스에서 unowned 프로퍼티와 다른 클래스에서 암시적으로 언래핑된(unwrapped) 옵셔널 프로퍼리를 결합(combine)하는 것이 유용합니다.

한번 초기화된 프로퍼티 모두 직접 접근(옵셔널 언래핑 없이)이 가능하며, 순환 참조를 피할수 있습니다. 이 섹션은 이러한 관계를 설정하는 방법을 보여줍니다.

아래는 각각 다른 클래스의 인스턴스를 프로퍼티로 저장하는 2개의 클래스 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에 대한 초기화로부터 호출됩니다. 하지만, 2단계 초기화(Two-Phase Initialization)에서 설명된 것 처럼, Country에 대한 초기화는 새로운 Country인스턴스가 완전히 초기화 될때까지, City초기화에 self를 전달할 수 없습니다.

이러한 요구사항을 처리하기 위해, Country의 capitalCity프로퍼티를 암시적으로 언래핑한 옵셔널 프로퍼티로 선언하며, 타입의 끝에 느낌표(!)로 표시합니다. 이는 capitalCity프로퍼티가 다른 옵셔널 처럼, 기본 값으로 nil로 가지고 있는 것을 의미하지만, 암시적으로 언래핑한 옵셔널(Implicitly Unwrapped Optionals)에서 설명된것 처럼, 값을 언래핑하지 않고서도 사용할 수 있습니다.

capitalCity는 기본 값 nil을 가지며, 새로운 Country 인스턴스는 초기화에 의해 name 프로퍼티를 설정하자 마자 초기화 된것으로 간주됩니다. 이것은 Country 초기화는 name프로퍼티가 설정되자마자 암시적인 self프로퍼티를 참조하고 전달할 수 있다는 것을 의미합니다. Country초기화는 Country초기화가 자신만의 capitalCity 프로퍼티를 설정할때, City초기화에 대한 매개변수중 하나로 self를 전달할수 있습니다.

이 모든 것들은 하나의 문장에서 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"

위 예제에서, 암시적으로 언래핑된 옵셔널의 사용은 2단계 클래스 초기화 요구사항을 모두 만족하는 것을 의미합니다. capitalCity 프로퍼티는 여전히 강한 순환 참조를 피하면서, 사용될수 있고 초기화가 완료되면 옵셔널이 아닌 값처럼 접근할수 있습니다.

클로져에 대한 강한 순환 참조(Strong Reference Cycles for Closures)

위에서 2개의 클래스 인스턴스 프로퍼티가 각기 서로에 대해 강한 참조를 유지할때, 강한 순환 참조가 생성될수 있는 방법을 보았습니다. 또한, weak와 unowned참조를 사용하여 이러한 강환 순환 참조를 깨트리는 방법을 보았습니다.

또한, 강한 순환 참조는 클래스 인스턴스의 프로퍼티에 클로져(closure)를 할당하는 경우에 발생할 수 있고, 클로져의 본문은 인스턴스를 캡쳐 합니다. 이 캡쳐는클로져의 본문에서 self.someProperty처럼, 인스턴스의 프로퍼티에 접근하거나 self.someMothod() 처럼, 클로져가 인스턴스에서 메소드를 호출하기 때문에 발생할 수 있습니다. 어느 경우에나, 이러한 접근은 클로져가 self를 캡쳐해서, 강한 순환 참조를 만듭니다.

강한 순환 참조는 클로져가 클래스 처럼, 참조 타입(reference type)이기 때문에 발생합니다. 프로퍼티에 클로져를 할당할때, 클로져에 참조(reference)를 할당합니다. 본질적으로, 위와 같은 문제입니다 - 두개의 강한 참조는 각각에서 유지됩니다. 하지만, 두개의 클래스 인스턴스가 아닌, 이번에는 클래스 인스턴스와 클로져가 각각 서로를 유지시킵니다.

Swift는 이문제에 대해 클로져 캡쳐 목록(closure capture list)라는 멋진(elegant) 해결젝을 제공합니다. 하지만, 클로져 캡쳐 목록으로 강한 순환 참조를 깨는 방법을 배우기 전에, 이러한 순환이 발생될 수 있는 원인을 이해하는 것이 유용합니다.

아래 예제는 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프로퍼티를 정의합니다. HTMLElement는 HTML 요소에서 그릴수 있는 텍스르르 표현하는 문자열을 설정할 수 있는, 옵셔널 text프로퍼티를 정의합니다.

이러한 간단한 두가지 프로퍼티 외에도, HTMLElement클래스는 lazy프로퍼티 asHTML로 정의합니다. 이 프로퍼티는 name와 text를 HTML문자열 조각으로 결합하는 클로져를 참조합니다. asHTML 프로퍼티는 () -> String타입이거나, 매개변수 없고 String값을 반환하는 함수입니다.

기본적으로, asHTML프로퍼티는 HTML 태그를 문자열 표현을 반환하는 클로져를 할당합니다. 이 태그가 존재하는 경우에, 옵셔널 text값을 포함하거나, 존재하지 않는 경우 텍스트 컨텐츠가 없습니다. 단락(paragraph) 요소에 대해서, 클로져는 text프로퍼티가 "some text"와 같거나 nil인지에 따라, "<p>some text</p>"또는 "<p />를 반환합니다.

asHTML프로퍼티는 인스턴스 메소드 처럼 이름 지어지고 사용됩니다. 하지만, asHTML은 인스턴스 메소드보다는 클로져 프로퍼티이기 때문에, , 특정 HTML 요소들에 대한 HTML 랜더링(rendering)을 변경하는 경우에, 사용자정의 클로져로 asHTML프로퍼티의 기본 값을 교체 할수 있습니다.

예를들어, asHTML 프로퍼티는 text프로퍼티가 nil인 경우에, 빈 HTML 태그를 반환하지 못하게 막기 위해, 기본 텍스트를 나타내는 클로져를 설정할 수 있습니다.

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프로퍼티는 요소가 실제 일부 HTML 출력 대상의 문자열 값으로 랜더링해야 하는 경우에만 필요하기 때문에, lazy프로퍼티로 선언되어 있습니다. 사실 asHTML은 기본 클로져에서 self를 참조 할 수 있는 lazy 프로퍼티인 것을 의미합니다. lazy 프로퍼티는 초기화가 완료되고 self가 존재할때까지 접근되지 않을 것입니다.

HTMLElement 클래스는 새로운 요소를 초기화 하기 위해, name인자와 text인자를 받아 하나의 초기화를 제공합니다. 또한, 클래스는 HTMLElement인스턴스가 메모리에서 해제될때 메시지를 출력하는 해제(deinitializer)를 정의합니다.

다음은 HTMLElement 클래스를 사용해서 새 인스턴스를 만들고 출력하는 방법입니다.

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

주의
위의 paragraph 변수는 옵셔널(optional) HTMLElement로 정의 되어 있으며, 강한 순환 참조의 존재를 보여주기 위해, nil로 설정할 수 있습니다.

공교롭게도, 위에서 작성했던것처럼, HTMLElement 클래스는 HTMLElement 인스턴스와 기본적으로 asHTML 값을 사용하는 클로져 사이에 강한 순환참조가 만들어 집니다. 어떻게 순환참조가 되는지 봅니다.

인스턴스의 asHTML프로퍼티는 클로져에 대해 강한 참조를 가지고 있습니다. 하지만, 클로져는 본문에서 self를 참조(self.name과 self.text 참조)하고 있기 때문에, 클로져는 self를 캡쳐(captures)하며, HTMLElement인스턴스에 대해 강한 참조를 가지고 있는 것을 의미합니다. 강한 순환 참조는 둘 사이에서 생성됩니다. (클로져에서 캡쳐링하는 값에 대한 더 자세한 정보는 캡쳐링하는 값(Capturing Values)을 보세요)

주의
클로져가 self를 여러번 참조하더라도, HTMLElement인스턴스에 대한 강한 참조는 한번만 캡쳐합니다.

paragraph 변수가 nil로 설정하고 HTMLElement인스턴스에 대한 강한 참조가 깨지는 경우에, 강한 순환 참조이기 때문에, HTMLElement 인스턴스나 그 클로져는 메모리에서 해제 되지 않습니다.

paragraph = nil

HTMLElement 해제에서 HTMLElement 인스턴스가 해제되지 않는 것을 보여주는, 메시지가 출력되지 않는다는 것을 주의하세요.

클로져에 대한 강한 순환 참조 해결하기(Resolving Strong Reference Cycles for Closures)

클로져와 클래스 인스턴스간에 강한 순환 참조를 해결하기 위해 클로져 정의에서 캡쳐 목록(capture list)을 정의합니다. 캡쳐 목록은 클로져 본문에 하나 이상의 참조 타입을 캡쳐할때 사용하는 규칙을 정의합니다. 두 클래스 인스턴스 사이에 강한 순환 참조 처럼, 강한 참조보다는 weak 또는 unowned 참조로 캡쳐되도록 선언합니다. 코드의 다른 부분간의 관계에 따라 weak또는 unowned와 어울리는 것을 선택합니다.

주의
Swift는 클로져에서 self의 멤버를 참조할때마다 (그냥 somePropery 또는 someMothod()보다는) self.someProperty 또는 self.someMethod()를작성해야 합니다. 이것은 self를 캡쳐 가능하다는 것을 기억하는데 도움이 됩니다.

캡쳐 목록 정의하기(Defining a Capture List)

캡쳐 목록에서 각 항목은 (delegate = self.delegate! 처럼) weak 또는 unowned 키워드와 클래스 인스턴스에 대한 참조나 어떤 값으로 초기화된 변수를 조합합니다(pairing). 이러한 조합은 대괄호([])안에 콤마(,)로 구분되어 작성합니다.

캡쳐 목록이 제공되는 경우에, 클로져의 매개변수 목록과 반환 타입 앞에 위치시킵니다.

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

클로져가 매개변수나 반환타입을 지정하지 않는 경우에 컨텍스트에 의해 추론 될수 있기 때문에, 캡쳐 목록은 클로져의 시작 부분에 위치하며, in 키워드를 붙여 줍니다.

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

약한 참조와 소유하지 않는 참조(Weak and Unowned References)

클로져와 인스턴스가 항상 서로를 참조하고 같은 시간에 메모리에서 해제 될때, 클로져에서 캡쳐를 소유하지 않는(unowned) 참조를 정의합니다.

반대로, 나중에 어느 시점에라도 캡쳐된 참조가 nil이 될수 있을때, 캡쳐를 약한(weak) 참조로 정의합니다. 약한(weak) 참조는 항상 옵셔널 타입이고, 참조하는 인스턴스가 메모리에서 해제될때 자동으로 nil이 됩니다. 이것은 클로져의 본문에서 참조하는 인스턴스가 존재하는지 검사하는 것이 가능합니다.

주의
캡쳐된 참조가 절대 nil이 되지 않는 경우에는, weak참조보다는, 항상 unowned 참조로 캡쳐되야 합니다.

unowned 참조는 클로져에 대한 강한 순환참조(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]입니다.

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

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

캡쳐 목록으로 참조하는 방법입니다.

이번에, 클로져에 의한 self의 캡쳐는 unowned 참조이고, 캡쳐된 HTMLEleemnt인스턴스를 강하게 유지하지 않습니다. paragraph 변수의 강한 참조를nil로 설정하는 경우, 아래 예제에서 해제 메시지의 출력을 볼수 있는 것처럼 HTMLElement인스턴스는 메모리에서 해제됩니다.

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

캡쳐 목록에 대해 더 자세한 정보는 캡쳐 목록(Capture Lists)을 보세요.

반응형

'Swift > Language Guide' 카테고리의 다른 글

기본 연산자(Basic Operators)  (0) 2019.02.26
Advanced Operators  (0) 2018.09.18
Access Control  (0) 2018.09.18
Memory Safety  (0) 2018.09.18
Generics  (0) 2018.09.18
Protocols  (0) 2018.09.18
Extensions  (0) 2018.09.18
Nested Types  (0) 2018.09.18
Posted by 까칠코더
,