Swift

Automatic Reference Counting (ARC)

Phililip
728x90

안녕하세요.

 

이번에는 Swift 공식 문서에 있는 Automatic Reference Counting (줄여서 ARC) 문서를 읽고 정리해보려고 해요.

 


# 1. Overview

Swift는 앱의 메모리 사용을 관리하기 위해서 Automatic Reference Counting(ARC)를 사용합니다.

 

ARC는 필요 없는 클래스 인스턴스에 대해서 메모리를 해제해줍니다. 그래서 사용자는 메모리 관리에 대해서 걱정하지 않아도 돼요ㅎㅎ

 

그런데 몇몇 경우에 ARC가 메모리 관리를 하기 위해 코드와 코드 간의 더 많은 정보를 요구하는 경우가 있어요.

 

이런 상황과 더불어 앱의 모든 메모리를 ARC가 관리할 수 있도록 하는 방법에 대해 알아보려고 해요.

 

(Reference Counting은 클래스 인스턴스에만 적용됩니다. Struct와 enumeration은 value type이기 때문이죠!)

(밑에서부터는 클래스 인스턴스를 줄여서 인스턴스라고 부를게요!)

 

 

# 2. How ARC Works

인스턴스를 생성할 때마다, ARC는 그 인스턴스에 대한 정보를 메모리에 할당합니다.

 

만약 그 인스턴스가 더 이상 필요 없게 된다면, ARC는 할당하고 있던 메모리를 해제(free, deallocated)시켜 버리죠.

 

 

그런데 만약 인스턴스가 다른 곳에서 사용 중인데 ARC가 메모리에서 해제시켰다면, 그 인스턴스에 접근할 때 앱이 크래쉬가 나겠죠??

 

이를 방지하기 위해서 ARC는 인스턴스가 프로퍼티, 상수, 변수에서 그 인스턴스를 참조하고 있는지 트래킹을 해요.

 

그래서 만약 하나라도 그 인스턴스를 참조하고 있는 게 있다면 메모리 해제를 하지 않는 것이죠.

 

이것을 strong reference라고 부릅니다.

 

인스턴스가 프로퍼티, 상수, 변수에 할당되면 그 인스턴스에 strong reference(강한 참조)가 걸리고, strong reference가 유지되는 동안에는 메모리 해제를 하지 않는 것입니다.

 

 

 

# 3. ARC in Action

ARC 동작에 대해 예를 들어볼게요.

 

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? 타입을 가진 3개의 변수를 선언해줬어요. Optional 타입이기 때문에 처음에는 nil이 할당될 것입니다.

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

 

reference1에 새로운 Person 인스턴스를 할당하게 되면

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

 

initializer가 호출되면서 메시지가 출력되겠죠?

 

reference1에 인스턴스를 할당했기 때문에 Person 인스턴스는 reference1한테 strong reference가 걸려있습니다.

 

 

아래처럼 나머지 변수들에도 동일한 인스턴스를 할당하게 되면, 2개의 추가 strong reference가 걸리게 되는 것이에요.

reference2 = reference1
reference3 = reference1

 

 

사실 중요한 건 메모리가 잘 해제되냐겠죠?

 

2개의 변수에 nil을 할당함으로써 strong reference를 끊어줄게요.

reference1 = nil
reference2 = nil

 

 

reference3한테 걸려있는 strong reference까지 끊어주면, deintializer가 잘 호출되는 것을 볼 수 있습니다.

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

 

 

 

## 4. Strong Reference Cycles Between Class Instances

위에서 ARC가 reference count를 보고 메모리 해제하는 것을 살펴봤습니다.

 

그런데, 만약 2개의 인스턴스가 서로를 참조하고 있다면 인스턴스의 reference count가 0이 되는 일이 없겠죠??

 

이런 상황을 strong reference cycle이라고 부릅니다.

 

 

strong reference cycle을 해결하기 위해서는 strong reference 대신 weak reference 또는 unowned reference를 사용해야 하는데, 자세한 건 뒤에서 얘기할게요.

 

 

지금은 순환 참조가 발생하는 상황 중 하나를 예로 들어볼게요.

 

아래같이 Apartment 인스턴스를 프로퍼티로 가지는 Person 클래스와, 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 인스턴스와 Apartment 인스턴스를 만들어주고, 프로퍼티까지 설정해주면

var john: Person?
var unit4A: Apartment?

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

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

 

참조 관계는 이렇게 되겠죠?

 

 

 

그다음에 각 변수에 nil을 설정해주면,

john = nil
unit4A = nil

 

인스턴스와 변수 간의 strong reference는 끊어지지만 Person 인스턴스와 Apartment 인스턴스 간의 strong reference는 끊어질 수가 없어요... (메모리 누수로 이어집니다ㅠㅠ)

 

메모리 누수...

 

이게 바로 strong reference cycle입니다...ㅠㅠ

 

 

 

# 5. Resolving Strong Reference Cycles Between Class Instances

Swift는 strong reference cycles를 해결하기 위해 2가지 방법을 제시하고 있어요.

  • weak references
  • unowned references

 

weak references, unowned references를 사용하면 강한 참조를 사용하지 않고도 참조가 가능하기 때문에 strong reference cycle이 발생하지 않습니다.

 

 

참조할 인스턴스의 lifetime이 자기 자신보다 짧을 때는 weak reference를 사용하고,

 

반대로 참조할 인스턴스의 lifetime이 자기 자신과 동일하거나 더 길 때는 unowned reference를 사용합니다.

 

 

## 5.1 Weak References

weak reference(약한 참조)는 인스턴스를 참조하긴 하지만 강하게 참조하는 것은 아니에요.

 

프로퍼티나 변수 선언 앞에 weak 키워드를 붙이면 약한 참조를 하겠다는 의미입니다.

 

 

약한 참조를 하고 있으면 ARC로 인해 참조하고 있는 인스턴스의 메모리가 자동으로 해제될 수 있는데, 이때는 nil이 자동으로 할당됩니다.

 

그래서 약한 참조를 사용할 거면 상수 대신 변수를 사용해야 합니다. (nil로 설정될 수 있기 때문이죠ㅎ)

 

참고: weak reference로 인해 ARC가 nil로 설정했을 경우에는 Property Observer가 호출되지 않습니다.

 

 

위에서 strong reference cycle이 발생한 예시를 weak reference로 수정해볼게요.

(Apartment.tenant를 weak 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 }
    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 인스턴스를 약하게 참조

 

 

이때 john 변수를 nil로 설정해준다면

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

 

Person 인스턴스를 강하게 참조하고 있는 것이 어디에도 없기 때문에 Person 인스턴스는 메모리 해제되고 tenant 프로퍼티에는 nil이 설정될 것이에요.

 

Person 인스턴스 메모리 해제

 

 

그다음에 unit4A 변수를 nil로 설정한다면, Apartment 인스턴스를 강하게 참조하고 있는 것이 없기 때문에 메모리 해제가 될 것입니다.

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

 

Apartment 인스턴스 메모리 해제

 

 

 

## 5.2 Unowned References

unowned reference도 weak reference와 동일하게 인스턴스를 강하게 참조를 하지 않습니다.

 

아래 사항들이 weak reference 하고 다른 점입니다.

  • 참조할 인스턴스의 lifetime이 자신과 동일하거나 더 길 때 사용합니다.
  • unowned reference는 value가 항상 보장된다고 생각하기 때문에, ARC는 unowned reference의 value에 nil을 절대로 할당하지 않습니다.

 

프로퍼티나 변수 선언 앞에 unowned 키워드를 붙이면 unowned reference를 사용하겠다는 의미입니다.

 

 

주의: 참조할 인스턴스의 메모리가 해제되지 않는 것이 보장될 때만 unowned reference를 사용해주세요.

 

 

unowned reference 관련 예시를 들어볼게요.

 

아래처럼 Customer 클래스와 CreditCard 클래스를 만들고, CreditCard 클래스는 unowned 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") }
}

Customer(고객) 클래스는 CreditCard(신용카드)를 가지고 있을 수도 있고 없을 수도 있으니 변수 + OptionalCreditCard 인스턴스를 받은 것이고

 

CreditCard(신용카드) 클래스는 신용카드를 만들 때 신용카드의 소유자가 반드시 있기 때문에 상수 + unownedCustomer 인스턴스를 받은 것입니다.

(CreditCard 인스턴스의 lifetime보다 Customer 인스턴스의 lifetime이 더 길어요.)

 

이러한 이유들 때문에 CreditCard initializer 파라미터에 Customer 인스턴스를 포함했어요.

(CreditCard 인스턴스 안에는 Customer 인스턴스가 있어야 하기 때문이죠!)

 

 

아래처럼 변수에 인스턴스를 할당해주면,

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

 

참조 관계는 이렇게 되겠죠?

 

 

 

이때 만약 john 변수에 nil을 할당하게 된다면, 아래처럼 Customer 인스턴스에 걸려있던 strong reference가 끊어집니다.

 

 

그럼 Customer 인스턴스는 ARC로 인해 메모리 해제되겠죠? Customer 인스턴스가 메모리 해제됨과 동시에 CreditCard 인스턴스에 걸려있던 strong reference가 끊어지기 때문에 CreditCard 인스턴스도 같이 메모리가 해제될 것입니다.

(strong reference cycle 해결!!)

 

 

참고
  지금까지 말했던 것들은 사실 safe unowned reference입니다. (runtime 때 메모리를 계속 감시하고 메모리가 해제된 인스턴스를 참조할 때 크래쉬를 내주기 때문에 safe 하다는 것이에요.)
  성능을 더욱 높이기 위해서 Swift는 unsafe unowned reference도 제공하고 있어요. unsafe unowned reference를 사용하면 참조하는 인스턴스의 메모리가 해제됐든 안됐든 크래쉬 없이 그냥 참조값(주소값)에 접근합니다. (당연히 valid 한 참조라는 것은 개발자가 책임을 져야겠죠?) unsafe unowned reference를 사용하려면 프로퍼티나 변수 선언 앞에 unowned(unsafe)를 써주면 됩니다.

 

 

 

## 5.3 Unowned Optional References

unowned 클래스에 optional 설정을 해줄 수도 있어요.

 

unowned optional reference를 사용할 경우에는 참조하는 인스턴스가 값이 있는지 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(강좌)를 저장하기 위해 Course 클래스에 대해 강하게 참조하고 있어요. 

 

Course(강좌) 클래스는 2개의 unowned reference를 가지고 있습니다.

- department 

- nextCourse

 

강좌와 관련된 부서는 반드시 존재하기 때문에 department 프로퍼티를 unowned reference로 설정했고, (not optional)

 

현재 강좌의 다음 심화과정을 나타내는 nextCourse 프로퍼티는 unowned optional reference로 설정했어요.

(심화과정이 없을 수도 있기 때문에 optional로 설정한 것입니다.)

 

 

위에서 생성한 클래스는 아래처럼 사용이 가능합니다.

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]

 

참조 관계는 이렇게 되겠죠?

 

(가장 오른쪽 Course 인스턴스의 name은 "p" -> "Caring for Tropical Plants"이 되어야 합니다!)

 

 

만약 Department 인스턴스에서 강좌를 하나 제거시키면, 제거된 Course 인스턴스는 strong reference가 모두 끊어지기 때문에 ARC로 인해 메모리가 해제되고, 그와 관련된 nextCourse 프로퍼티는 자동으로 nil이 설정됩니다.

 

 

unowned optional reference와 weak reference은 서로 유사하지만, unowned optional reference를 사용하면 "인스턴스가 있을 수도 있고 없을 수도 있지만 만약 인스턴스가 있다면 그 인스턴스의 lifetime은 나보다는 길 것이다!" 라는 것을 강조하는 것이라고 이해를 했어요...

 

 

 

## 5.4 Unowned References and Implicitly Unwrapped Optional Properties

weak reference를 설명할 때 사용했던 PersonApartment 예시는 strong reference cycle을 피하기 위해 서로 Optional 한 값을 사용했습니다.

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

 

 

그리고 unowned reference를 설명할 때 사용했던 CustomerCreditCard 예시는 strong reference cycle을 피하기 위해 한쪽은 Optional, 그리고 다른 한쪽은 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") }
}

 

 

그런데 만약 서로가 nil이 될 수 없도록 해야 하는 상황이라면 어떻게 해야 할까요?

 

이런 상황은 unowned propertyimplicitly unwrapped optional property(! 붙인 거)를 조합해서 해결할 수 있어요.

 

 

아래가 그 예시입니다.

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

 

Country(국가)는 반드시 CapitalCity(수도)를 가져야 하고, City(도시)는 도시가 속한 Country(국가)가 있어야 한다는 것을 표현한 것입니다.

 

 

City 인스턴스를 생성할 때는 Country 인스턴스가 필요해요. 그리고 Country 인스턴스를 생성할 때 City 인스턴스를 같이 생성해주는데 이때 자기 자신(self)을 넘겨줍니다.

 

원래라면 Country 인스턴스가 완전히 initialized 될 때까지 Country의 initializer에서 자기 자신(self)을 넘겨줄 수 없지만 지금은 가능해요.

 

capitalCity 프로퍼티를 City! 타입으로 선언했기 때문입니다! => implicitly unwrapped optional property

 

음? 그게 뭐 어쩌라고....? 🤔

 

implicitly unwrapped optional property의 기본값은 nil입니다. 그래서 Country.name 만 초기화가 된다면 Country 인스턴스는 모두 초기화가 되었다고 판단하는 것이고, 그 말은 즉 initializer 내부에서 self를 사용할 수 있다는 것이에요!!

 

아하!!

 

 

그래서 인스턴스 생성에 아무런 문제도 없고, non-optional 한 값도 사용하며, strong reference cycle도 피할 수 있는 것입니다.

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"

 

 

 

# 6. Strong Reference Cycles for Closures

인스턴스 프로퍼티에 클로저를 등록하고, 그 클로저 안에서 self를 사용해서 그 인스턴스를 사용하고 있다면 strong reference cycle이 발생할 수 있습니다.

(왜냐면 클로저도 reference type 이기 때문이에요.)

 

 

예를 들어서 아래처럼 사용할 경우에는

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

 

참조가 서로 강하게 걸리기 때문에 strong reference cycle이 발생하게 되는 것이죠.

 

 

 

그럼 어떻게 해결해야 할까요?

 

답은 바로 아래에 있습니다ㅎㅎㅎ

 

 

 

# 7. Resolving Strong Reference Cycles for Closures

capture list를 클로저를 정의할 때 같이 정의함으로써 strong reference cycle을 해결할 수 있어요.

 

capture list는 클로저 안에서 하나 이상의 reference types를 사용(capture)할 때 어떻게 사용할지에 대한 규칙을 정하는 것인데요, 이를 통해서 strong reference를 weak나 unowned reference로 선언이 가능합니다.

 

 

참고: Swift는 클로저 안에서 self의 프로퍼티나 method를 사용해야 할 때, someProperty나 someMethod 보다는 self.someProperty나 self.someMethod처럼 사용하는 것을 권장합니다. (왜냐면 클로저 내부에서 self를 사용하고 있다는 것을 명시적으로 보여주기 위함입니다.)

 

 

## 7.1 Defining a Capture List

Capture list를 어떻게 쓰냐?

 

Capture list 안에 있는 아이템들은 인스턴스를 참조(ex. self)하거나 변수 초기화(ex. 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
}

 

 

## 7.2 Weak and Unowned References

클로저와 클로저가 캡처하는 인스턴스가 서로 참조를 하고 항상 동시에 deallocated 될 때는 unowed reference로 캡처를 정의합니다.

 

이와 반대로, 캡처한 인스턴스가 nil이 될 가능성이 있다면 weak reference로 캡처를 정의합니다. 당연히 참조하는 인스턴스가 deallocated 된다면 nil로 자동 할당됩니다.

 

 

참고: 캡처한 인스턴스가 nil이 되는 경우가 없다면, weak보단 unowned reference를 권장합니다.

 

 

위에서 예로 든 HTMLElement 클래스에서 strong reference cycle이 발생하지 않으려면 아래처럼 수정하면 되겠죠??

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

 

 

 

 

## 참고

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

 

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

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

 

 


 

이번 글은 여기서 마무리.

 

긴 글 읽어주셔서 감사합니다.

 

 

반응형

'Swift' 카테고리의 다른 글

클로저에서 [weak self] 사용할 때 주의할 점2  (0) 2022.04.24
클로저에서 [weak self] 사용할 때 주의할 점  (0) 2022.04.16
subscript  (0) 2022.04.03
Struct와 Class  (0) 2022.04.02
FormatStyle  (0) 2022.03.24