티스토리 뷰

 

 

Automatic Reference Counting — The Swift Programming Language (Swift 5.5)

Automatic Reference Counting Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, this means that memory management “just works” in Swift, and you don’t need to think about memory management your

docs.swift.org

 

Automatic Reference Counting

Swift 는 앱의 메모리를 추적하고 관리하기 위해 자동 참조 카운팅(Automatic Reference Counting, ARC) 를 사용합니다. 대부분의 경우, ARC는 Swift에서 메모리 관리가 "그냥 작동한다" 라는 것을 의미합니다. 사용자는 메모리관리에 대해 생각할 필요가 없습니다. ARC는 인스턴스가 더 이상 필요하지 않을 떄 자동적으로 메모리를 비웁니다.

 

그렇지만 몇 몇 경우에서 ARC는 메모리 관리를하기 위해 코드의 사이 관계에 대한 더 많은 정보를 요구합니다. 이 챕터는 이러한 상황들을 설명하고 어떻게 ARC 가 사용자의 모든 앱 메모리를 관리하는지 보여줍니다. 

 

참조 카운팅(Reference Counting) 은 클래스의 인스턴스에만 적용됩니다. 구조체와 열거형은 값타입(value types) 이기 때문에 참조에 의해 저장되고 전달되지 않습니다.

 

 

 

 

How ARC Works (ARC 작동 원리)

사용자가 새로운 클래스 인스턴스를 생성할 때마다, ARC는 인스턴스 정보를 저장하기 위해 메모리의 상당량을 할당 합니다. 메모리는 인스턴스의 타입, 인스턴스와 연관된 모든 저장 프로퍼티(stored properties) 값 정보를 보관합니다.

 

인스턴스가 더 이상 필요없을 때, ARC는 해당 인스턴스가 사용하던 메모리를 비웁니다. 이에 따라 메모리는 다른 목적을 위해 사용될 수 있습니다. 이는 클래스 인스턴스가 필요하지 않을 때 메모리에서 공간을 차지하지 않는다는 것을 의미합니다.

 

만약 ARC가 사용 중인 인스턴스를 할당 해제한다면, 해당 인스턴스의 프로퍼티와 메소드를 사용할 수 없습니다. 더해서 인스턴스에 접근하려고 하면 앱이 충돌합니다.

 

인스턴스가 사용되는 동안 없어지는 것을 막기위해, ARC는 몇 개의 프로퍼티, 상수, 변수들이 지금 각 클래스 인스턴스를 참조하는지 추적합니다. ARC는 해당 인스턴스에 대한 활성(active) 참조가 하나라도 남아있는 경우 인스턴스를 할당 해제하지 않습니다.

 

이를 가능하게 하기 위해 프로퍼티, 상수, 변수에 클래스 인스턴스를 할당할 경우, 해당 프로퍼티, 상수, 변수는 인스턴스에 강한 참조(strong reference)를 가지게 됩니다. "강한" 참조라 불리는 이유는 해당 인스턴스를 강하게 잡고 있기 때문입니다. 해당 강한 참조가 남아있으면 할당 해제가 허락되지 않습니다.

 

 

 

 

 

ARC in Action (ARC 사용)

아래는 ARC 가 어떻게 사용되는지에 대한 예제입니다.

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

Person 클래스는 name 프로퍼티를 설정하고 출력하는 이니셜라이져를 가지고 있습니다. 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 클래스 이니셜라이져를 호출함에 따라 "John Appleseed is being initialized" 가 출력되었습니다. 이를 통해 초기화가 이루어졌음을 확인합니다.

 

새 Person 인스턴스가 reference1 변수에 할당됬기 때문에, reference1 은 새 Person 인스턴스에 강한 참조를 가집니다. 하나 이상의 강한 참조가 있기 때문에, ARC는 이 Person을 메모리에 보관하고 할당 해제되지 않게 합니다.

 

만약 같은 Person 인스턴스를 다른 2개의 변수에 할당한다면, 강한 참조가 두 개 더 생성됩니다.

reference2 = reference1
reference3 = reference1

 

이제 단일 Person 인스턴스에 3 개의 강한 참조가 있습니다.

 

변수에 nil을 할당해 3개 중 2개의 강한 참조를 없앤다 해도, 1개의 강한 참조가 남아있기 때문에 Person 인스턴스는 할당 해제되지 않습니다.

reference1 = nil
reference2 = nil

 

ARC는 Person 인스턴스의 마지막 강한 참조가 없어질 때까지 할당해제 하지 않습니다. 즉 Pesron 인스턴스를 더 이상 사용하지 않는 것이 분명할 때 까지 말입니다.

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

 

 

 

 

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

위 예제에서 ARC는 새 Person 인스턴스에 대한 참조의 개수를 추적할 수 있습니다. Person 인스턴스가 더 이상 필요하지 않을 때 할당해제 할 수도 있습니다.

 

"클래스의 인스턴스가 강한 참조를 안 가지는 것"을 못 하게 코드를 작성할 수 있습니다. (계속 강한 참조를 가지게 된다는 뜻)

이는 두 클래스 인스턴스가 서로 강한 참조를 가지고 있어 다른 인스턴스를 활성(alive) 상태로 유지하는 경우 발생할 수 있습니다. 이는 강한 참조 사이클(Strong reference cycle) 이라고 알려져 있습니다.

 

클래스 사이의 관계를 약한 참조(weak reference) 나 미소유참조(unowned reference) 로 정의함으로써 강한 참조 사이클을 해결합니다. 그러나 강한 참조 사이클을 어떻게 해결하는지 배우기 전에 어떻게 사이클이 발생하는 지 이해하면 유용합니다.

 

아래 예제는 어떻게 강한 참조 사이클이 발생하는지 보여줍니다. 

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 프로퍼티(String 타입)와 옵셔널 apartment 프로퍼티(기본값 nil) 를 가집니다. 

 

모든 Apartment 인스턴스는 unit 프로퍼티(String 타입)와 옵셔널 tenant프로퍼티(기본값 nil) 를 가집니다.

 

두 클래스 모두 디이니셜라이져를 정의합니다. 이를 통해 Person 및 Apartment 인스턴스가 할당해제 되면 출력을 확인할 수 있습니다.

 

아래 코드는 john (Person? 타입) 과 unit4A (Apartment? 타입) 라는 옵셔널 타입 변수를 정의합니다. 두 변수 모두 nil 값을 가집니다.

var john: Person?
var unit4A: Apartment?

 

특정 Person 인스턴스와 Apartment 인스턴스를 생성하고 john 과 unit4A 에 할당할 수 있습니다.

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

 

아래 그림은 두 인스턴스가 생성되고 할당된 다음 강한 참조가 어떻게 되있는지 보여줍니다.

john 변수는 새 Person 인스턴스에 강한 참조를 가지고, unit4A 변수는 새 Apartment 변수에 강한 참조를 가집니다. 

출처 : Swift 공식문서

person이 apartment를 가지고 apartment 가 tenant를 가지도록 두 인스턴스를 연결할 수 있습니다. john과 unit4A 옵셔널 변수에 저장되어 있는 인스턴스에 접근하기 위해 강제 언래핑 ! 합니다.

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

 

두 인스턴스를 연결하면 강한 참조는 아래와 같은 모습을 보입니다.

출처 : Swift 공식 문서

불행히도 두 인스턴스 사이의 연결이 강한 참조 사이클을 만듭니다.

Person 인스턴스는 Apartment 인스턴스에 강한 참조를 가지고, Apartment 인스턴스는 Person 인스턴스에 강한 참조를 가집니다. john과 unit4A가 가지고 있는 강한 참조를 없애면, 참조 개수는 0 이 됩니다. ARC는 인스턴스를 할당 해제하지 않습니다.

john = nil
unit4A = nil

 

두 변수를 nil 로 설정했을 때, 디이니셜라이져가 호출되지 않았습니다. 강한 참조 사이클은 Person 인스턴스는 Apartment 인스턴스가 할당 해제되는 것을 막고, 앱에 메모리 누수를 유발합니다.

 

아래 그림은 john과 unit4A 를 nil로 설정한 후, 강한 참조를 가지고 있는 모습입니다.

출처 : Swift 공식 문서

Person 인스턴스와 Apartment 인스턴스 사이의 강한 참조가 없어지지 않고 남아 있습니다.

 

 

 

 

 

Resolving Strong Reference Cycles Between Class Instances (클래스 인스턴스 사이의 강한 참조 사이클 해결)

클래스 타입의 프로퍼티로 작업할 때, Swift 는 강한 참조 사이클을 해결하기 위해 2가지 방식을 제공합니다

약한 참조(weak reference)

미소유 참조(unowned reference)

 

약한 참조와 미소유 참조는 참조 사이클에서 "한 인스턴스가 다른 인스턴스를 강하게 잡지(hold) 않으면서 참조"할 수 있게 합니다. 그러면 인스턴스는 강한 참조 사이클을 생성하지 않고 서로를 참조할 수 있습니다.

 

약한 참조는 다른 인스턴스가 짧은 생명주기(lifetime) 을 가질 때 사용합니다. 이는 다른 인스턴스가 먼저 할당해제 될때를 말합니다.

Apartment 예제에서, apartment의 생명주기 중에 tenant를 가지지 않게 할 수 있습니다. 따라서 이 경우, 약한 참조는 강한 참조 사이클을 없애기 위한 적절한 방식입니다. 반대로 다른 인스턴스가 같은 생명주기나 더 긴 생명주기를 가질 때 미소유 참조를 사용합니다.

 

 

 

Weak Reference (약한 참조)

약한 참조는 참조하는 인스턴스를 강하게 잡지 않는 참조입니다. 따라서 ARC가 참조된 인스턴스 처리를 중단하지 않습니다. 이 동작은 참조가 강한 참조 사이클이 되는 것을 막습니다. weak 키워드를 사용해 약한 참조를 나타냅니다.

 

약한 참조는 참조하는 인스턴스를 강하게 잡지 않기 때문에, 약한 참조가 참조하고 있는 중에도 인스턴스를 할당해제 할 수 있습니다. ARC는 참조하는 인스턴스가 할당 해제되면, 자동으로 약한 참조를 nil로 설정합니다. 

약한 참조는 런타입 중 값을 nil로 바꿔야 하므로, 항상 상수말고 옵셔널 타입의 변수로 선언됩니다.

 

다른 옵셔널 값과 마찬가지로 약한 참조에 값이 있는지 확인할 수 있습니다. 더 이상 존재하지 않는 invalid 인스턴스에 대한 참조로 끝나지 않습니다.

 

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

 

아래 예제는 위에서 다뤘던 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") }
}

 

john과 unit4A 두 변수 사이의 강한 참조와 두 인스턴스의 연결이 이전과 같이 생성됩니다.

var john: Person?
var unit4A: Apartment?

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

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

 

아래 그림은 두 인스턴스를 연결했을 때의 참조를 보여줍니다.

출처 : Swift 공식 문서

Person 인스턴스는 여전히 Apartment 인스턴스에 강한 참조를 가지고 있습니다. 하지만 Apartment 인스턴스는 Person 인스턴스에 약한 참조를 가집니다. 이는 john 변수를 nil로 설정해 강한 참조를 없앨 경우, 더 이상 Person 인스턴스에 강한 참조가 없다는 것을 의미합니다.

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

 

Person 인스턴스에 대해 더 이상 강한 참조가 없기 때문에, Person 인스턴스는 할당 해제되고 tenant 프로퍼티는 nil로 설정됩니다.

출처 : Swift 공식 문서

Apartment 인스턴스에 남아 있는 강한 참조는 unit4A 변수에 남아 있는 참조 뿐입니다. 해당 강한 참조를 없애면 Apartment 인스턴스에는 더 이상의 강한 참조가 없습니다.

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

 

Apartment 인스턴스에 더 이상 강한 참조가 없기 때문에, 두 개 모두 할당 해제됩니다.

출처 : Swift 공식 문서

 

garbage 컬렉션을 사용하는 시스템에서 약한 포인터는 간단한 캐싱 구조(caching mechnism) 을 구현하기 위해 사용됩니다. 강력한 참조가 없는 object는 메모리 압력이 garbage collection을 trigger 하는 경우에만 할당이 취소되기 때문입니다. 그러나 ARC의 경우, 값은 마지막 강한 참조가 제거되는 즉시 할당이 해제되므로, 약한 참조를 해당 목적에 사용하는 것은 적합하지 않습니다.

 

 

 

 

 

Unowned References (미소유 참조)

약한 참조와 같이 미소유 참조(unowned references) 는 참조하는 인스턴스를 강하제 붙잡지(hold) 않습니다.

그러나 약한 참조와는 다르게 미소유 참조는 "다른 인스턴스들이 같은 생애주기나 더 긴 생애주기를 가질 때" 사용합니다. unowned 키워드를 사용해 미소유 참조를 나타냅니다.

 

약한 참조와 다르게 미소유 참조는 항상 값을 가지는 것으로 기대됩니다. 결과적으로 미소유(unowned)로 값을 표시하면 옵셔널을 만들지 않습니다. ARC는 미소유 참조 값을 nil로 설정하지 않습니다.

 

참조가 항상 할당 해제되지 않은 인스턴스를 참조하는 경우에만 미소유 참조를 사용합니다.

인스턴스가 할당해제 된 후 미소유 참조의 값에 접근하면 런타임 에러가 발생합니다.

 

아래 예제는 Customer와 CreditCard 두 클래스를 정의합니다. 두 클래스는 각각 다른 클래스의 프로퍼티 인스턴스를 저장합니다. 이 관계는 강한 참조 사이클을 생성할 가능성이 있습니다.

 

Customer와 CreditCard 의 관계는 약한 참조예제에서 보았던 Apartment & Person 의 관계와 조금은 다릅니다. 이 데이터 모델에서 Customer는 CreditCard 가 없을 수 있지만, CreditCard 는 항상 customer가 있어야 합니다.

CreditCard 인스턴스는 참조하는 Customer 인스턴스 밖에서 존재할 수 없습니다. 이를 표현하기 위해 Customer 클래스는 옵셔널 card 프로퍼티를 가지지만 CreditCard 클래스는 미소유(unowned) 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 프로퍼티는 Int 타입이 아닌 UInt64 타입으로 정의됩니다. 16개의 카드 숫자가 32bit, 64bit 시스템에서 모두 저장가능할 만큼 충분히 큰지 확인합니다.

 

특정 고객에 대한 참조를 저장하기 위해 옵셔널 Customer 변수 john을 선업합니다. 초기값은 nil입니다. 

var john: Customer?

 

이제 Customer 인스턴스를 생성할 수 있습니다. Customer 인스턴로 새 CrediCard를 초기화하고 할당해서, card 프로퍼티로 사용합니다.

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

 

아래 그림은 연결된 두 인스턴스 사이의 참조를 보여줍니다.

출처 : Swift 공식 문서

Customer 인스턴스는 CreditCard 인스턴스에 강한 참조를 가집니다. CreditCard 인스턴스는 Customer 인스턴스에 미소유 참조를 가집니다.

 

미소유 customer 참조때문에, john 변수가 가지고 있는 강한 참조를 없애면 더 이상 Customer 인스턴스에 강한 참조가 남아있지 않습니다.

출처 : Swift 공식 문서

Customer 인스턴스에 강한 참조가 없기 때문에, Customer 인스턴스는 할당해제됩니다. 이 일이 일어나고 CreditCard 인스턴스에도 더 이상 강한 참조가 없기 때문에, CreditCard 인스턴스도 할당해제됩니다.

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

john을 nil로 설정하면 두 인스턴스 모두 디이니셜라이져 메시지를 출력합니다. 이를 통해 Customer 인스턴스의 강한 참조를 없애면 둘 다 할당 해제되는 것을 확인할 수 있습니다.

 

위 예제는 안전한(safe) 미소유 참조 사용을 보여줍니다. Swift는 안전하지 않은(unsafe) 미소유 참조도 제공합니다. 런타임안전 검사를 비활성화해야 하는 경우에 말입니다. 예를 들어 퍼포먼스적인 이유에서 말입니다. 모든 unsafe한 작업처럼 코드의 안정성을 확인할 책임이 있습니다.

unowned(unsafe)를 작성해 안전하지 않은 미소유 참조(unsafe unowned reference)를 나타냅니다. 참조하는 인스턴스가 할당해제 된 후에 unsafe unowned reference에 접근하려고 하면, 프로그램이 인스턴스가 있던 메모리 위치에 접근하려고 시도합니다. 이는 안전하지 않은 작업입니다.

 

 

 

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

클래스에 옵셔널 참조를 미소유로써 표시할 수 있습니다. ARC 소유권 모델(ownership model)의 관점에서 미소유 옵셔널 참조(unowned optional reference)와 약한 참조 둘 다 같은 문맥으로 사용될 수 있습니다. 차이점이라 하면, 미소유 옵셔널 참조를 사용할 때 항상 유효한 object를 참조하가나 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에 대해 강한 참조를 유지합니다. ARC 소유권 모델에서 department는 courses를 소유합니다.

Course는 두 개의 미소유 참조를 가집니다. 하나는 department에, 다른 하나는 nextCourese에. 

course는 이 둘 중 어느 것도 소유하지 않습니다. 모든 course는 어떤 department의 일부입니다. 따라서 department 프로퍼티는 옵셔널이 아닙니다. 단 nextCoures는 옵셔널 입니다.

 

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]

 

 

코드는 1개의 department 와 3개의 Course를 생성합니다. intro와 intermediate 는 nextCourse 프로퍼티에 다음 코스를 저장합니다. 학생이 해당 course를 완료한 후 미소유 옵셔널 참조를 유지해야 합니다.

출처 : Swift 공식 문서

미소유 옵셔널 참조는 감싸고 있는 클래스의 인스터스를 강하게 붙잡지 않습니다. 따라서 인스턴스를 할당 해제하는 ARC를 막을 수 없습니다.

ARC 아래의 미소유 참조가 동작하는 방식과 같은 방식으로 작동합니다. 단, 미소유 옵셔널 참조는 nil이 될 수 있습니다. 

 

non-옵셔널 미소유 참조처럼, nextCourse가 항상 할당 해제되지 않는 course를 참조해야 할 책임이 있습니다. 이 경우에 department.courses 에서 course를 지운다면, 다른 coureses 에 있을 수 있는 참조도 삭제해야 합니다.

 

 

옵셔널 값의 기본 타입은 옵셔널입니다. 그러나 옵셔널이 값타입을 unowned로 표시할 수 없는 것은 규칙에 예외입니다.

클래스를 감싸는 옵셔널은 참조 카운팅을 사용하지 않습니다. 따라서 옵셔널에 강한 참조를 유지할 필요가 없습니다.

 

 

 

 

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

위에서 다뤘던 약한 참조와 미소유 참조에 대한 예제는 강한 참조를 없앨 필요가 있는 흔한 시나리오를 다뤘습니다.

 

Person과 Apartment 예제는 (nil이 허용되는) 두 프로퍼티가 강한 참조를 발생시킬 가능성이 있는 상황을 보여줬습니다. 이 시나리오는 약한 참조로 해결하는 것이 가장 좋습니다.

 

Customer 와 CreditCard 예제는 하나의 프로퍼티는 nil 이 될 수 있지만 다른 하나는 nil이 허용되지 않는 상황에서 강한참조가 일어날 가능성이 있다는 것을 보여줬습니다. 이 시나리오는 미소유 참조로 해결하는 것이 가장 좋습니다.

 

그러나 세 번째 시나리오가 있습니다. 두 프로퍼티 모두 항상 값을 가져야 하며, 초기화가 완료된 후에는 nil 값이 되면 안되는 경우 입니다. 이 시나리오에서 한 클래스에 미소유 프로퍼티(unowned property)를 사용하고 다른 한 클래스에는 암묵적 언래핑 옵셔널 프로퍼티(implicitly unwrapped optional property)를 사용해 결합하는 것이 유용합니다.

 

이는 초기화가 완료된 후 두 프로퍼티에 직접적으로 접근할 수 있게 해줍니다 (옵셔널 언래핑 없이). 참조 사이클을 피하면서 말입니다. 이 장은 이러한 관계를 설정하는 방법을 설명합니다.

 

아래의 예제는 Country와 City 클래스를 정의합니다. 이 데이터 모델에서 country는 반드시 capital 를 가져야 하며, 모든 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
    }
}

두 클래스 간의 상호의존성(interdependency)를 설정하려면, City의 이니셜라이져는 Country 인스턴스를 가지며, 이 인스턴스를 country 프로퍼티에 저장합니다.

 

City 이니셜라이져는 Country의 이니셜라이져에서 호출됩니다. 그러나 Country 이니셜라이져는 Country 이니셜라이져가 완전히 초기화 될 때까지 City 이니셜라이져에 self를 전달할 수 없습니다.

 

이 요구사항에 대응하기 위해 Country의 capitalCity 프로퍼티를 암묵적 언래핑 옵셔널 프로퍼티(implicitly unwrapped optional property)로 선언합니다. 타입 뒤에 !를 작성해 이를 나타냅니다. (City!)

이는 capitalCity 프로퍼티가 nil의 기본값을 가진다는 것을 말합니다. 하지만 언래핑 없이 그 값에 접근이 가능합니다.

 

capitalCity는 기본 값으로 nil을 가지기 때문에, 새 Country 인스턴스는 "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"

위 예제는 암시적 언래핑 옵셔널의 사용이 두 단계의 클래스 이니셜라이져 요구사항을 충족시키는 것을 의미합니다.

capitalCity 프로퍼티는 초기화가 한 번 끝나면 non-옵셔널 값처럼 접근하고 사용할 수 있습니다. 강한 참조 사이클을 피하면서 말입니다.

 

 

 

 

 

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

위에서 두 클래스 인스턴스 프로퍼티가 서로 강한 참조로 붙잡고 있을 때, 어떻게 강한 참조 사이클이 생성됐는지 봤습니다. 이 강한 참조 사이클을 깨기 위해 어떻게 약한 참조(weak reference)와 미소유 참조(unowned references)를 사용하는지 봤습니다.

 

클래스 인스턴스의 프로퍼티에 클로져를 할당하고 그 클로져의 본문이 인스턴스를 캡쳐하고 있으면,  강한 참조 사이클이 발생할 수 있습니다. 이 캡쳐는 아마 발생합니다. 클로져의 본문이 인스턴스의 프로퍼티에 접근하기 때문입니다. 혹은 클로져가 인스턴스에 메소드를 호출하기 때문입니다. 둘 중 어느 경우에도, 이러한 접근들은 클로져가 self 를 "capture" 하게 만들고 강한 참조 사이클을 생성합니다.

 

이 강한 참조 사이클은 클로져가 참조 타입(reference types)이기 때문에 발생합니다. 프로퍼티에 클로져를 할당할 때, 해당 클로져에 참조를 할당합니다. 본질적으로 위와 같은 문제입니다. - 두 강한 참조가 서로를 붙잡고 있는 문제

이번에는 두 클래스 인스턴스가 아닌, 하나의 클래스 인스턴스와 하나의 클로져가 서로를 붙잡고 있습니다. 

 

Swift는 이 문제를 풀기 위해 우아한 해결책을 제공합니다. closuer 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")
    }

}

HTMLElement 클래스는 name 프로퍼티를 정의합니다. text 프로퍼티도 정의합니다. 

 

HTMLElement 클래스는 lazy 프로퍼티 asHTML도 정의합니다. 이 프로퍼티는 name과 text를 HTML 문자열 조각으로 결합하는 클로져를 차조합니다. asHTML 프로퍼티는 () -> String 타입입니다.

 

기본적으로 asHTML 프로퍼티는 클로져를 할당합니다. 클로져는 HTML 태그를 표현하는 문자열을 리턴합니다. 이 태그는 옵셔널 text 값이 존재한다면 포함하고, text 값이 존재하지 않으면 텍스트 내용이 없습니다.

문단(Paragraph) 요소의 경우, 클로져는 텍스트 프로퍼티가 "some text" 혹은 nil 인지 여부에 따라 "<p>some text</p>" 나 "<p />"를 리턴합니다.

 

asHTML 프로퍼티는 인스턴스 메소드처럼 이름이 지정되고 사용됩니다. 그러나 asHTML은 인스턴스 메소드이기보다 클로져 프로퍼티이기 때문에, asHTML 프로퍼티의 초기값을 사용자 지정(custom) 클로져로 대체할 수 있습니다. 

 

예를 들어 asHTML 프로퍼티는 클로져로 설정될 수 있습니다. 클로져는 text 프로퍼티가 nil이라면 "some default text" 로 기본 설정 됩니다. 빈 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 프로퍼티는 lazy 프로퍼티로 정의되었습니다. 요소를 실제로 HTML 출력 대상에 대한 문자열 값으로 렌더링 할 때에만 필요하기 때문입니다. asHTML이 lazy 프로퍼티라는 사실은 기본 클로져로 self를 참조할 수 있다는 뜻입니다. lazy 프로퍼티는 초기화가 완료되고 self 가 존재한다고 알려질 때까지 접근할 수 없기 때문입니다. 

 

HTMLElement 클래스는 단일 이니셜라이져를 제공합니다. name 인수 (필요하다면) 와 text 인수를 가집니다. 클래스는 디이니셜라이져도 제공합니다. HTMLElement가 할당해제 됬을 때 메시지를 출력해 보여줍니다.

 

아래 코드는 새 인스턴스를 생성하고 출력하기 위해 어떻게 HTMLElement 클래스를 사용하는지 보여줍니다.

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

 

paragraph 변수는 옵셔널 HTMLElement로 정의되었습니다. 따라서 강한 참조 사이클의 존재를 입증하기 위해 nil로 설정될 수 있습니다.

 

나쁘게도, 위에 작성된 HTMLElement 클래스는 "HTMLElement 인스턴스"와 "기본 asHTML 값을 위해 사용되는 클로져" 사이에 강한 참조 사이클을 생성합니다.

출처 : Swift 공식 문서

인스턴스의 asHTML 프로퍼티는 클로져에 강한 참조를 가집니다.

그러나 클로져가 본문의 self를 참조하고 있기 때문에, 클로져는 self를 캡쳐(capture) 합니다. 이는 HTMLElement 인스턴스에 강한 참조가 있다는 뜻입니다. 강한 참조 사이클이 둘 사이에 생성됩니다.

 

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

 

만약 HTMLElement 인스턴스의 강한 참조를 없애기 위해 paragraph 변수를 nil로 설정한다면, HTMLElement 인스턴스나 해댕 클로져는 강한 참조 사이클 때문에 할당 해제되지 않습니다.

paragraph = nil

HTMLElement 디이니셜라이져의 메시지가 출력되지 않습니다. 이를 통해 HTMLElement 인스턴스가 할당 해제되지 않은 것을 보여줍니다.

 

 

 

 

 

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

클로져의 정의의 일부로 캡쳐 리스트(capture list)를 정의 한다면 클래스 인스턴스와 클로져 사이의 강한 참조 사이클을 해결할 수 있습니다. 캡쳐 리스트는 클로져의 본문에 하나 상의 참조 타입을 캡쳐하는 규칙을 정의합니다. 

두 클래스 인스턴스 사이의 강한 참조 사이클에서, 각각의 캡쳐된 참조를 약한 참조 혹은 미소유 참조로 정의했습니다. 약한 참조나 미소유 참조에 대한 적절한 선택은 코드의 다른 부분 사이 관계에 따라 달라집니다.

 

Swift는 그냥 someProperty, someMethod() 로 작성하는 것보다 self.someProperty, self.someMethod()로 작성하는 것을 요구합니다. 클로져 내에서 자신의 멤버를 참조할 때마다 말입니다. 이는 self를 사고로 캡쳐하는 것이 가능하다는 것을 기억하도록 도와줍니다.

 

 

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

캡쳐 리스트(Capture List) 안에 있는 각 항목은 "클래스 인스턴스에 대한 참조로 weak나 unowned 키워드를 짝으로 구성"하거나 "어떤 값으로 초기화된 변수" 입니다 (delegate = self.delegate 같이). 이 짝은 [ ] 안에 , 로 구별되서 작성됩니다.

 

클로져 파라미터와 리턴 타입(만약 제공된다면) 전에 캡쳐 리스트를 작성합니다.

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 (약한 참조와 미소유 참조)

클로져 안에 미소유 참조로써 캡쳐(capture)를 정의합니다. 클로져와 캡쳐한 인스턴스가 서로를 항상 참조할 것이고 항상 같은 시간에 할당을 해제할 때 말입니다.

 

반대로, 캡쳐 참조가 미래의 특정 지점에 nil이 될 것 같을 때, 약한 참조로써 캡쳐를 정의합니다. 약한 참조는 항상 옵셔널 타입입니다. 첨조하는 인스턴스가 할당해제되면 자동으로 nil이 됩니다. 이를 통해 클로져 본문 내에 존재하는지 확인할 수 있습니다.

 

만약 캡쳐 참조가 nil이 될 수 없다면, 약한 잠조보다는 미소유 참조로 캡쳐되야 합니다.

 

아래 코드는 사이클을 피하기 위해 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>"

 

아래 그림은 캡쳐 목록이 있는 참조를 보여줍니다.

출처 : Swift 공식 문서

이번에 클로져에 의한 self의 캡쳐는 미소유 참조입니다. 캡쳐한 HTMLElement 인스턴스에 대해 강한 참조를 가지지 않습니다. 만약 paragraph 변수의 강한 참조를 nil로 설정한다면 HTMLElement 인스턴스는 할당해제 됩니다. 그리고 디이니셜라이져가 메시지를 출력하는 것을 볼 수 있습니다.

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

 

 

 

 

 

ARC (Automatic Reference Counting), Swift의 자동 참조 카운팅

메모리를 관리하기 위해 Swift 가 알아서 참조를 할당하고 해제한다.

두 클래스 인스턴스 사이에 강한 참조 사이클(strong reference cycle) 이 발생한다.

이를 약한 참조(weak reference) 와 미소유 참조 (unowend reference) 로 해결한다.

클래스와 클로져 사이에도 강한 참조 사이클이 발생 한다.

이 경우 약한 참조와 미소유 참조로 구성된 캡쳐 리스트를 통해 해결한다.

 

 

 

 

 

ARC 정복 완.료. 😎

 

 

 

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함