GGURUPiOS

Swift 공식 문서 정리 - (25) Automatic Reference Counting (ARC 본문

Swift/공식문서 정리 ( 문법 )

Swift 공식 문서 정리 - (25) Automatic Reference Counting (ARC

꾸럽 2023. 5. 14. 15:51

Automatic Reference Counting ( 자동 참조 카운팅 )

스위프트에서는 앱의 메모리 사용을 관리 하기위해 ARC를 사용함

자동으로 참조 횟수를 관리하기 때문에 개발자는 메모리 관리에 신경 쓸 필요가 없음

하지만 몇몇의 경우 ARC에서 메모리 관리를 위해 코드의 특정 부분에 대한 관계에 대한 정보를 필요로 함

참조 횟수는 클래스 타입의 인스턴스에만 적용되고 값 타입인 구조체 열거형 등에는 적용 X

ARC의 동작

클래스의 새 인스턴스를 만들 때마다 ARC는 인스턴스 정보를 담는데 필요한 적당한 크기의 메모리를 할당함

이 메모리는 그 인스턴스에 대한 정보와 관련된 저장 프로퍼티 값도 가지고 있음

추가적으로 인스턴스가 더이상 사용되지 않을때 ARC는 그 인스턴스가 차지하고 있는 메모리를 해지해서 다른 용도로 사용할 수 있도록 공간을 확보함

하지만 만약 ARC가 아직 사용중인 인스턴스를 메모리에서 내렸는데 인스턴스의 프로퍼티에 접근한다면 앱은 크래시가 발생하게 됨

ARC에서는 아직 사용중인 인스턴스를 해지하지 않기 위해 얼마나 많은 프로퍼티, 상수 혹은 변수가 그 인스턴스에 대한 참조를 갖고있는지 추적함

그래서 ARC는 최소 하나라도 그 인스턴스에 대한 참조가 있는 경우 그 인스턴스를 메모리에서 해지하지 않음

→ 요약: 클래스의 인스턴스 정보를 메모리에 저장하고, 그 메모리를 참조하고 있는 프로퍼티나 상수 혹은 변수가 1개 이상이라면 메모리에서 해지하지 않음 (에러방지)

ARC의 사용

클래스가 생성, 해지 될 때 print 하는 클래스

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

위에서 선언한 Person 클래스 타입을 갖는 reference 변수 3개 선언

하나의 변수에 Person 인스턴스를 생성해서 참조하도록 하고, 나머지 두 변수를 첫 번째 변수를 참조하도록 함

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

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

이 경우 2,3 모두 처음에 1이 참조하고 있는 Person인스턴스를 참조하게 됨

이 시점에 Person인스턴스의 참조 횟수는 3이 됨

그리고 나서 1,2 의 참조를 해지해도 참조 횟수는 1이기 때문에 해지되지 않음 ( reference 3이 남이있기 때문 )

reference1 = nil
reference2 = nil

그 이후에 3의 참조를 해지하면 ARC가 person 인스턴스를 메모리에서 해지하게 됨

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

클래스 인스턴스간 강한 참조 순환

앞의 예제에서 ARC에서 기본적으로 참조횟수에 대해 추적하고 있기 때문에 더이상 사용하지 않는 인스턴스는 자동 해제 됨

하지만 절대로 메모리에서 해제 되지 않는 경우도 있음

예를들어, 클래스의 인스턴스간 강하게 상호참조를 하고 있는 경우가 바로 그 경우임

이 경우는 강한 참조 순환이라 알려져 있음

예를들어 어떻게 강한 참조 순환이 발생하는지 알아보자

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 클래스는 변수로 Apartment 인스턴스를 소유하고 있고 그 Apartment 클래스 에서는 변수로 Person 형의 인스턴스를 소유하고 있음

만약 다음과 같이 변수 선언 후 인스턴스를 생성하면?

var john: Person?
var unit4A: Apartment?

선언한 변수에 각가에 맞는 타입의 인스턴스를 새엇ㅇ

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

현재까지의 변수와 인스턴스 상태를 보면 다음과 같음

변수 john은 Person의 인스턴스를 참조하고 있고 변수 unit4A는 Apartment 인스턴스를 참조 중

이 상황에서 john의 apartment 변수에 unit4A를 unit4A.tenant에 john을 할당해보자

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

인스턴스 안의 apartment와 tenant가 각각 Apartment, Person 인스턴스를 참조하고 있는 상황이 됨

Person인스턴스의 참조횟수는 2, Apartment의 인스턴스 참조횟수도 2가 됨

이 시점에서 각 변수에 nil을 할당해 참조를 해지해보자

의도한것은 각 변수가 참조하고 있던 Person과, Apartment인스턴스가 해지되는 것임

그러나 이 두 인스턴스는 해지 되지 않는다

john = nil
unit4A = ni 

각 변수에 nil을 할당한 상황에서 참조 상황은 위의 그림과 같음

john과 unit4A는 각 인스턴스에 대한 참조를 하고 있지 않지만 Person 인스턴스와 Apartment인스턴스의 변수가 각각 상호 참조를 하고 있어 참조 횟수가 1이기 때문에 이 두 인스턴스는 해지되지 않고 메모리 누수 발생함

→ 즉, john의 apartment에는 unitA4가, unitA4의 tenant에는 john을 참조하고 있기 때문에 자동해지 안 됨

클래스 인스턴스간 강한 참조 순환의 문제 해결

앞에서 본 강한 참조 순환문제 해결은 두가지 방법이 있음

  • weak 참조 ( 약한 참조 )
  • unowned 참조 ( 미소유 참조 )

두 참조 모두 ARC에서 참조 횟수를 증가시키지 않고 인스턴스를 참조함.

그래서 강한 참조 순환 문제를 해결할 수 있음

약한 참조는 참조하고 있는 인스턴스가 먼저 메모리에서 해제될 때 사용하고

미소유 참조는 반대로 참조하고 있는 인스턴스가 같은 시점 혹은 더 뒤에 해제될 때 사용

→ 무슨 말일까 ? → 미소유 참조는 내가 사용할 때는 그 인스턴스가 무조건 nil 이 아닐 때 사용한다.

위의 예제에서 Apartment의 tenant는 없는 상태가 될 수 있기 때문에 ( 먼저 해지 되기 때문에 ) 약한 참조를 사용하는 것이 적절함

약한 참조 (Weak References)

약한 참조로 선언하면 참조하고 있는 것이 먼저 메모리에서 해제 되기 때문에 ARC는 약한 참조로 선언된 참조 대상이 해지되면 런타임에 자동으로 참조하고 있는 변수에 nil 을 할당함

예제를 살펴보자

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 인스턴스의 변수에서 각각 인스턴스를 상호 참조하도록 할당

var john: Person?
var unit4A: Apartment?

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

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

그러면 아래와 같은 참조 상황이 되고, 앞선 예제와 다른 점은 Apartment의 tenant변수가 Person 인스턴스를 약한 참조로 참조하고 있다는 점임

그래서 이 시점에 Person 인스턴스에 대한 참조 횟수는 변수 john이 참조하고 있는 1회 뿐임

그래서 john의 참조대상을 nil로 할당하면 더 이상 Person 인스턴스를 참조하는 것이 없게 되고, 자동으로 메모리에서 해지함

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

Apartment 인스턴스를 참조하는 개체도 사라지게 돼서

Apartment 인스턴스도 메모리에서 해지됨

미소유 참조

미소유 참조는 약한 참조와는 다르게 참조 대상이 되는 인스턴스가 현재 참조하고 있는 것과 같은 생애주기를 갖거나 더 긴 생애주기 를 갖기 때문에 항상 참조에 그 값이 있다고 기대됨

그래서 ARC는 미소유 참조에는 절대 nil을 할당하지 않음

다시 말하면 미소유 참조는 옵셔널 타입을 사용하지 않음

→ 정말 쉽게 이해하자면, 해당 인스턴스를 !를 통해 강제 언래핑 했다고 생각하자.

예제를 보자

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는 card 변수로 CreditCard 인스턴스를 참조하고 있고 CreditCard는 customer로 Customer 인스턴스를 참조하고 있음

customer는 미소유 참조 unowned로 선언함.

이유는 고객과 신용카드를 비교해 봤을 때 신용카드는 없더라도 사용자는 남아있을 것이기 때문

다시말하면 사용자는 항상 존재. 그래서 CreditCard에 customer를 unowned로 선언함

이제 고객 변수 john을 옵셔널 타입으로 선언

var john: Customer?

선언한 고객에 인스턴스를 생성하고 고객의 카드 변수에도 카드 인스턴스를 생성해 할당

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

이 시점에서의 참조 상황은 아래와 같음

john이 Customer 인스턴스를 참조하고 있고 CreditCard인스턴스도 Customer Instance를 참조하고 있지만 미소유참조 하기 때문에 Customer 인스턴스에 대한 참조횟수는 1회가 됨

이 상황에서 john 변수의 Customer 인스턴스 참조를 끊으면 다음 그림과 같이 변함

그러면 더이상 Customer 인스턴스를 강하게 참조하고 있는 인스턴스가 없으므로 Customer 인스턴스가 해제되고 그에 따라 CreditCard 인스턴스를 참조하고 있는 개체도 사라지므로 CreditCard 인스턴스도 메모리에서 해제됨

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

클래스에 대한 옵셔널 참조를 미소유 참조로 표시할 수 있음

ARC 소유권 모델 측면에서 미소유 옵셔널 참조와 약한 참조는 모두 동일한 컨텍스트에서 사용될 수 있음

차이점은 미소유 옵셔널 참조를 사용할 때 항상 유효한 인스턴스를 참조하거나 설정되어 있는지 확인해야 할 책임이 있다는 것

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는 course를 가짐

Course는 두개의 미소유 참조를 가지고 있고, 하나는 department에 다른 하나는 학생이 수강해야 하는 다음 course에 대한 것임

course에는 두가지 인스턴스에 대한 소유권이 없다.

모든 각 코스는 department의 일부분이기 때문에 department의 프로퍼티는 옵셔널이 아님

그러나 몇몇 course는 권장되는 후속과정이 없으므로, 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]

위의 코드는 department 1개의 인스턴스와 해당하는 세개의 course를 만듬

intro, intermediate는 모두 nextCourse에 저장되어 있으며 이 프로퍼티는 미소유 옵셔널 참조를 유지함

위의 그림과 같은 상황임

미소유 옵셔널 참조는 래핑되는 클래스의 인스턴스를 강하게 유지하지 않으므로

ARC가 인스턴스의 할당을 해제하는 것을 막지 않음

이는 ARC에서 미소유 참조와 동일하게 동작하지만 미소유 옵셔널 참조가 nil일 수 있다는 점을 제외하고 동일 함.

옵셔널이 아닌 미소유 참조와 마찬가지로, 사용자는 nextCourse가 항상 할당 취소되지 않은 과정을 참조하는지 확인해야 함

예를들어, 위의 경우에서 department.course에서 과정을 삭제할 때 다른 과정에 대한 참조도 제거해야함

옵셔널 값의 기본 타입은 Optional 이며, 이는 스위프트 표준 라이브러리의 열거형임

그러나 옵셔널은 값 유형을 미소유로 표시할 수 없다는 규칙의 예외 임

미소유 참조와 암시적 옵셔널 프로퍼티 언래핑

위의 약한 참조와 미소유 참조에 대한 예에서는 강력한 참조 순환을 중단해야 하는 일반적인 상황 중 두 가지를 다룸

Person 및 Department 예제에서는 두 가지 속성이 모두 nil일 수 있으며 강한 참조 순환을 발생시킬 수 있는 상황을 보여줌 → 이 상황에는 weak 참조로 해결하는 것이 가장 좋다

Customer 및 CreditCard 의 예는 nil이 허용된 하나의 프로퍼티와 nil 이 될 수 없는 다른 프로퍼티가 강력한 참조 순환을 유발할 가능성이 있는 상황을 보여줌. 이 상황은 미소유 참조로 해결하는 것이 가장 좋음

그러나 세 번째 상황(밑에 나옴)에서는 두 프로퍼티가 항상 값을 가져야하며 초기화가 완료되면 두 속성 모두 nil이 되어서는 안됨. 이 상황에서는 한 클래스의 미소유 프로퍼티와 다른 클래스의 암묵적으로 래핑되지 않은 옵셔널 프로퍼티를 결합하는 것이 유용함

이를 통해 초기화가 완료되면 두 프로퍼티에 모두 직접 액세스 할 수 있으며 참조 순환을 피할 수 있음

아래예제는 두 개의 클래스를 정의하며 각 클래스는 다른클래스의 인스턴스를 속성으로 저장함

이 데이터 모델에서 모든 국가는 항상 수도를 가져야하며, 모든 도시는 항상 국가에 속해야함

이를 나타내기 위해 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의 이니셜라이저가 City 이니셜라이저에게 자신(self)을 전달할 수 없음

이 요구 사항을 충족하기 위해 Country의 CapitalCity 프로퍼티를 타입 선언 끝에 느낌표로 표시된 암묵적으로 래핑되지 않은 옵셔널 프로퍼티로 선언함

즉 CapitalCity 프로퍼티는 다른 옵셔널과 마찬가지로 기본값이 nil 이지만 값을 풀 필요 없이 액세스 할 수 있음

capitalCity는 기본값이 nil이므로 Country인스턴스가 이니셜라이저 내에서 name 프로퍼티를 설정하는 즉시 새 Country 인스턴스가 완전히 초기화된 것으로 간주됨

즉, Country 이니셜라이저는 name 프로퍼티가 설정되는 즉시 암묵적인 자체 프로퍼티를 참조하고 전달 가능함

따라서 Country 이니셜라이저는 Country 이니셜라이저가 자체 CapitalCity 속성을 설정할 때 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"

capitalCity 프로퍼티는 초기화가 완료되면 옵셔널이 아닌 값처럼 사용하고 액세스 할 수 있으며,

강력한 참조 주기는 피할 수 있음

클로저에서의 강한 참조 순환

강한 참조 순환은 변수 뿐만 아니라 클로저와 관계돼서 발생할 수 있음

클로저에서는 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")
    }
}

/*
HTMLElment 클래스의 클로저 asHTML은 입력값을 받지 않고 반환 값이 String 인 () -> String
클로저를 사용함 
이 클로저 안에서 self.text 와 self.name과 같이 self를 캡쳐하게 됨
*/

// 아래와 같이 변경 될 수도 있음
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>"

→ 아래와 같이 인스턴스와 클로저 간에 강한 참조를 하기 때문에 강한 참조 순환에 빠지게 됨

→ 간단하게 말해서 클로저가 self (여기서는 paragraph 를 강하게 참조하므로) 인스턴스를 해제해도 클로저가 붙잡고 있어서 해제가 안됨

paragraph의 참조를 nil 로 할당하더라도 HTMLElement 인스턴스는 해제되지 않음

클로저에서 강한 참조 순환 문제의 해결

클로저에서 강한 참조 순환 문제를 해결하기 위해 캡쳐 참조에 강한 참조 대신

weak, unowned 참조를 지정할 수 있음

두개의 사용여부는 코드에서의 상호 관계에 달려있음

캡쳐 리스트 정의

캡쳐리스트를 정의하기 위해서는 클로저의 파라미터 앞에 소괄호 ([]) 를 넣고 그 안에 각 캡쳐 대상에 대한 참조 타입을 적어줌

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

같은 시점이나 나중 시점에 해제되는 경우는 unowned 참조를 사용

자 이제 클로저에 적절한 캡쳐 리스트를 적어 코드를 실행해 보도록 하자

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

// 클로저 앞에 [unowned self] 를 작성해주었음

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

이번에는 self 의 캡쳐가 미소유 참조이며, 캡쳐한 HTMLElement 인스턴스를 강력하게 유지하지 못함

문단 변수의 강력한 참조를 nil로 설정하면 HTMLElement의 인스턴스 할당이 취소 됨