ARC (Automatic Reference Counting)
Swift는 ARC를 사용하여 앱의 메모리 사용을 관리하고 추적한다. 대부분의 경우, Swift에서 메모리 관리란 "그냥 되는 것" 정도이고, 메모리 관리를 위해 별도로 생각할 필요가 없다. 왜냐하면 ARC가 자동적으로 쓸모가 없어진 클래스 인스턴스를 메모리에서 지워버리기 때문이다!
하지만, 몇몇 경우에 ARC는 메모리를 관리하기 위한 코드들 간의 관계에 대한 더 많은 정보를 요구하기도 한다. 이번 장에서는 ARC가 모든 앱의 메모리를 관리할 수 있도록 하는 것을 보여줄 것이다.
레퍼런스 카운팅(Reference counting)은 클래스의 인스턴스에게만 적용된다. 구조체나 열거형은 참조 타입이 아닌 값 타입이기 때문에 참조에 의해 저장되거나 보내질 수 없다!
How ARC Works
클래스의 인스턴스를 생성할 때 마다, ARC는 인스턴스에 대한 정보를 저장하기 위해 메모리 덩어리를 할당한다. 이 메모리는 인스턴스의 타입에 대한 정보 뿐만 아니라, 인스턴스와 관련된 저장된 프로퍼티(stored property)의 값들도 가지고 있다.
추가적으로, 인스턴스가 더 이상 필요하지 않게 되면, ARC는 인스턴스가 사용한 메모리를 비우고, 다른 목적으로 사용할 수 있도록 도와준다. 즉 클래스 인스턴스가 필요없어진다면, 더 이상 메모리 공간을 차지하지 않게 되는 것이다.
하지만, 아직 사용 중인 인스턴스를 만약 ARC가 해제해버렸다면, 해당 인스턴스의 프로퍼티에 접근할 수 없고, 인스턴스의 메서드 또한 호출할 수 없다. 만약 이러한 상황에서 인스턴스에 접근한다면, 앱은 크래쉬가 나게 된다! 왜냐? 메모리에 없는 상태이기 때문이다!
인스턴스가 아직 필요할 때 메모리에서 사라지지 않게 하기 위해, ARC는 각 클래스 인스턴스를 현재 참조하는 프러퍼티, 상수, 변수들의 수를 추적한다. ARC는 하나의 참조라도 인스턴스에 존재한다면 해당 인스턴스를 해제하지 않는다.
이를 가능하게 하려면, 클래스 인스턴스를 프로퍼티, 상수 또는 변수에 할당했을 때, 프로퍼티, 상수 또는 변수가 인스턴스에 강한 참조(strong reference)
를 하도록 만들어줘야 한다. 인스턴스를 강하게 붙잡고, 강한 참조가 남아있다면 해제하는 것을 허용하지 않기에 "강한" 참조라고 부른다.
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
클래스는 인스턴스의 name
프로퍼티를 세팅하는 이를 메세지로 출력하는 이니셜라이저를 가지고 있다. 또한 클래스의 인스턴스가 해제되었을 때 메세지를 출력하는 디이니셜라이저를 가지고 있다.
아래에는 Person?
타입의 세 변수가 있다. 옵셔널 타입이기 때문에, 자동으로 nil 값으로 초기화되고, 아직은 Person
인스턴스를 참조하지 않는다.
var reference1: Person?
var reference2: Person?
var reference3: Person?
이제 Person
객체(인스턴스)를 만들 수 있다.
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
Person
클래스의 이니셜라이저가 호출되었기 때문에 "John Appleseed is being initialized"가 출력되는 것을 볼 수 있다. 이는 이니셜라이저가 제 때 잘 작동했다는 것을 의미한다.
새로운 Person
객체가 reference1
변수에 할당되었기 때문에, reference1
-> Person
으로의 강한 참조가 생긴다. 아직 1개의 강한 참조가 있기에 Person
은 메모리에서 해제되지 않는다.
같은 Person
객체를 다른 두 개의 변수에 할당한다면, 두 개의 강한 참조가 해당 객체에 추가적으로 생기게 된다.
reference2 = reference1
reference3 = reference1
이로써 단일 Person
객체에 강한 참조가 3개나 생기게 된다.
만약 두 개의 변수에 nil
을 할당하여 두 개의 강한 참조를 끊는다고 하면, 아직 하나의 강한 참조가 남게 되며, 그렇기에 Person
객체는 메모리에서 해제되지 않는다.
reference1 = nil
reference2 = nil
ARC는 마지막 강한 참조가 끊길 때까지 Person
객체를 메모리에서 해제하지 않으며, 메모리 해제를 하고 싶다면 마지막 참조 또한 끊어버리면 된다. 그러면 디이니셜라이저가 호출되는 것을 볼 수 있다.
reference3 = nil
// Prints "John Appleseed is being deinitialized"
Strong Reference Cycles Between Class Instances
위의 예시처럼, ARC는 Person
객체의 참조 횟수를 추적하고 더 이상 필요가 없다면 Person
객체는 메모리에서 해제된다.
하지만 클래스 인스턴스의 참조 횟수가 0이더라도 이를 해제하지 않을 수 있는 방법이 있다. 바로 두 개의 클래스 인스턴스들이 서로를 강한 참조로 가지고 있는 경우 참조 횟수가 0이 되어도 메모리에서 해제되지 않는데, 이를 강한 순환 참조(Strong Reference Cycle)
이라고 한다.
강한 순환 참조는 클래스들 간의 관계를 강한 참조 대신 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
인 tenant
프로퍼티를 갖고 있다. 마찬가지로 아파트의 주인이 항상 있지는 않기 때문에 tenant
는 옵셔널로 표기한다.
두 개의 클래스는 클래스의 인스턴스가 디이니셜라이즈 된다는 메세지를 갖고 있는 디이니셜라이저를 정의하고 있다. 이를 통해 Person
과 Apartment
가 메모리에서 해제되었는지 알 수 있다.
아래의 코드는 john
과 unit4A
라고 하는 옵셔널 변수를 보여준다. 마찬가지로 옵셔널이기에 초기값은 nil
이다.
var john: Person?
var unit4A: Apartment?
이제 Person
객체와 Apartment
객체를 john
과 unit4A
에 할당할 수 있다.
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
두 객체를 생성하고 할당 했을 때의 강한 참조를 봐보자. john
변수는 이제 새로운 Person
객체에 강한 참조를 갖고 있고, unit4A
변수는 새로운 Apartment
객체에 강한 참조를 갖고 있다.
이제 person이 apartment를 갖도록, apartment가 tenant를 갖도록 하여, 두 객체를 서로 연결할 수 있다. john
과 unit4A
는 옵셔널로 정의되어 있기 때문에 프로퍼티에 접근하려면 느낌표(!)
를 붙여서 언래핑해줘야 한다.
john!.apartment = unit4A
unit4A!.tenant = john
두 객체를 연결한 이후 강한 참조 관계는 다음과 같이 바뀐다.
불행하게도, 두 객체를 연결하게되면 이 둘 사이에서 강한 순환 참조
가 생기게 된다. Person
객체는 이제 Apartment
객체에 강한 참조를 가지고, Apartment
객체는 이제 Person
객체에 강한 참조를 가지게 된다. 그러므로, john
과 unit4A
변수의 강한 참조를 끊어도 (레퍼런스 카운트가 0이 되어도) 객체는 ARC에 의해 메모리 해제되지 않는다.
john = nil
unit4A = nil
마찬가지로 디이니셜라이저도 호출되지 않는다. 강한 순환 참조는 Person
과 Apartment
객체가 메모리 해제되는 것을 방지하는데, 이 때문에 앱에서 메모리 누수(memory leak)
가 발생하게 된다.
메모리 누수(memory leak) : 프로그램이 불필요한 메모리를 계속 점유하고 있는 현상. 할당된 메모리를 사용한 다음 반환하지 않는 것이 누적되면 메모리가 낭비된다. 즉 더 이상 불필요한 메모리가 해제되지 않으면서 메모리 할당을 잘못 관리할 때 발생한다.
john
과 unit4A
에 nil
을 할당했을 때 강한 참조는 아래와 같이 변경된다.
Person
과 Apartment
객체 사이의 강한 참조는 끊어지지 않고 계속 유지되게 된다.
Resolving Strong Reference Cycles Between Class Instances
Swift는 클래스 타입의 프로퍼티로 작업하고 있을 때, 강한 순환 참조를 해결하기 위한 두 가지 방법을 제공하고 있다. 바로 weak
참조와 unowned
참조이다.
weak
참조와 unowned
참조는 인스턴스가 강한 참조 없이 다른 인스턴스를 참조할 수 있도록 만들어준다. 즉, 강한 참조를 하지 않기에 강한 순환 참조를 만들지 않고도 객체는 참조할 수 있는 것이다.
다른(참조할) 객체가 더 짧은 생애주기(lifetime)을 가지고 있다면 weak
을 사용한다. (왜냐하면 다른 객체가 먼저 해제될 수 있기 때문이다.) Apartment
예시에서, 특정 시점에 apartment는 tenant를 가지고 있지 않을 수 있다. 그래서 이 경우에는 순환 참조를 깨기 위해서는 weak
가 적절한 해결법이 되는 것이다. 반대로, unowned
참조는 다른 객체가 동일한 생애주기를 갖거나 더 긴 생애주기를 가질 때 사용한다.
Weak References
weak
은 객체에 대한 강한 참조 없이 참조할 수 있게 해주고, 이 덕분에 ARC가 참조된 객체를 처리하는 것을 멈추지 않게 해준다. (= 메모리 해제를 방해하지 않는다.) 이러한 행위들이 강한 순환 참조를 막아준다. 프로퍼티나 변수의 선언 전에 weak
키워드를 써주면 약한 참조(weak reference)를 사용할 수 있다.
약한 참조는 객체에 대해 강한 참조를 하지 않기 때문에, 아직 약한 참조가 있는 와중에도 객체가 메모리 해제될 수 있다. 그러므로, ARC는 객체가 메모리 해제되면, 자동적으로 약한 참조를 nil
로 세팅한다. 그리고 약한 참조는 런타임에 값이 nil
로 변할 수 있음을 허용해줘야 하기 때문에 항상 상수(let
) 보다는 옵셔널 타입의 변수(var
)로 선언해줘야 한다.
기본적인 옵셔널 값에 접근하는 것 처럼 약한 참조에 값이 있는지 확인할 수 있고, 더 이상 존재하지 않는 객체에 대해서도 참조할 수 없다.
ARC가 약한 참조를 nil로 세팅했을 때, 프로퍼티 감시자(property observer)는 호출되지 않는다.
아래의 예시는, 위 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
이제 참조 관계가 어떻게 되었는지 살펴보자.
Person
객체는 여전히 Apartment
객체에 대해 강한 참조를 가지고 있지만, Apartment
객체는 Person
객체에 대해 약한 참조를 가지고 있다. 이는 즉, john
변수를 nil
로 세팅하여 강한 참조를 깼을 때, Person
객체에 대한 강한 참조는 더 이상 없게 된다.
john = nil
// Prints "John Appleseed is being deinitialized"
이제 Person
에 대한 강한 참조가 없기 때문에, 이는 메모리 해제되고 tenant
는 nil
이 된다.
여전히 남아있는 강한 참조는 unit4A
가 Apartment
객체를 참조하고 있는 것 뿐이다. 만약 이 강한 참조 또한 끊어버린다면, Apartment
객체에 대한 강한 참조는 더 이상 없게 된다.
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
마찬가지로, Apartment
객체에 대한 강한 참조가 남아있지 않기에 메모리에서 해제된다.
가비지 컬렉션(garbage collection)을 사용하는 시스템에서는, 강한 참조가 없는 객체는 메모리 압력(memory pressure)이 가비지 컬렉션을 촉진할 때만 메모리 해제되기 때문에, weak 포인터는 종종 간단한 캐싱 매커니즘을 만들 때 사용된다. 하지만 ARC를 사용하면 마지막 강한 참조가 제거되는 즉시 값이 메모리 해제되므로, 약한 참조가 이러한 목적으로는 적합하지 않다.
Unowned References
약한 참조와 유사하게, 미소유 참조는 객체에 대해 강한 참조를 하지 않는다. 하지만 약한 참조와 다르게, 미소유 참조는 다른 객체가 동일한 생애주기 혹은 더 긴 생애주기를 갖고 있을 때 사용된다. 미소유 참조는 프로퍼티나 변수의 선언 앞에 unowned
를 붙여서 사용할 수 있다.
약한 참조와 다르게, 미소유 참조는 "항상 값을 가지고 있을거라고" 예상한다. 즉 값이 무조건 있을 것이라는 것이다. 그렇기 때문에 unowned
가 붙은 값은 옵셔널이 될 수 없고, ARC가 미소유 참조의 값을 nil
로도 세팅할 수 없다.
절대로 해제되지 않을 객체를 항상 참조할 때 unowned를 사용한다.
만약 객체가 해제된 이후 미소유 참조의 값에 접근한다면, 런타임 에러를 일으키게 된다.
다음은 은행 고객과 고객에게 가능한 신용카드를 모델로 정의한 Customer
와 CreditCard
클래스에 대한 예시이다. 두 개의 클래스들은 다른 클래스의 인스턴스를 프로퍼티로써 저장하고 있다. 이 관계는 강한 순환 참조를 일으킬 가능성이 있다.
Customer
와 CreditCard
의 관계는 약한 참조에서 봤던 Apartment
와 Person
예시의 관계와는 조금 다르다. 이 데이터 모델에서, 고객은 카드를 가지고 있을 수도 있고 아닐 수도 있지만, 신용카드는 항상 고객과 연관되어있다. 생애주기로 봤을 때, CreditCard
객체는 CreditCard
가 참조하는 Customer
보다 더 오래 살 수 없다. 이를 표현하기 위해서 Customer
클래스는 card
프로퍼티를 옵셔널로 하고, CreditCard
클래스는 customer
프로퍼티를 미소유(옵셔널도 아니도록)로 선언해준다.
추가적으로, CreditCard
객체는 커스텀 초기화를 통해 number
값과 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") }
}
32비트와 64비트 시스템에서 16자리 카드 넘버를 수용하기 위해 CreditCard 클래스의 number 프로퍼티는 Int 보다는 UInt64로 정의하였다.
이제 john
이라는 옵셔널한 Customer
을 정의해준다. 이 변수는 옵셔널이기에 초기에는 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
에 대한 미소유 참조를 가지게 된다.
미소유 참조이기 때문에, john
변수에 대한 강한 참조를 끊게 되면 더 이상 Customer
가 가지는 강한 참조는 없게 된다.
Customer
객체에 더 이상 강한 참조가 없기 때문에, 이는 메모리 해제된다. 그 이후, CreditCard
객체에 강한 참조가 없기 때문에 이 또한 메모리 해제된다.
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
john
변수에 nil
이 할당되면서 Customer
객체와 CrreditCard
객체 가 디이니셜라이즈 되면서 메세지가 출력되는 것을 볼 수 있다.
위 예시는 safe한 미소유 참조에 대한 것이다. Swift는 성능상의 이유로 런타임에서 safety checks를 하지 않으려 할 때 unsafe한 미소유 참조 또한 제공한다. unsafe한 모든 작업과 마찬가지로 사용자는 해당 코드의 안전성을 검사해야한다.
unowned(unsafe)를 명시하여 unsafe한 미소유 참조를 사용할 수 있다. 만약 객체가 메모리 해제된 이후, unsafe한 미소유 참조에 접근하려고 한다면, 프로그램은 객체가 있던 메모리 위치에 접근하려고 시도하게 되는데, 이는 안전하지 않는 작업이다.
Unowned Optional References
클래스에 대한 옵셔널 참조를 미소유로 표기할 수 있다. ARC의 경우 미소유 참조와 약한 참조는 같은 맥락에서 사용될 수 있었다. 차이점은 미소유 참조를 사용할 때, 참조하고자 하는 객체가 항상 값을 가지고 있는지(유효한지) nil
은 아닌지 확인해야 한다는 것이다.
특정 학부(department)가 제공하는 코스(courses)들을 예시로 봐보자.
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
는 학부가 제공하는 각 coures들과 강한 참조를 가진다. ARC 관점에서 보면 학부가 강의를 소유한다는 것이다.
Course
는 두 개의 미소유 참조를 가지고 있는데, 하나는 department에 대한, 그리고 나머지는 학생이 들어야 하는 nextCourse에 대한 것이다. 또한 모든 코스는 학부에 속하기 때문에 department
프로퍼티는 옵셔널이 아니다. 하지만, 어떤 코스들은 추천 강의가 없을 수 있기에, 즉 nextCoure
프로퍼티는 옵셔널이 된다.
이 클래스를 사용한 예시를 봐보자.
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개의 courses를 생성한다. intro와 intermediate 코스는 모두 다음 코스를 제공하기에 nextCourse
프로퍼티에 이를 저장하고 있고, 다음에 수강해야하는 코스에 대해 미소유 옵셔널 참조를 유지하고 있다.
ARC에서 미소유 옵셔널 참조는 미소유 참조와 동작 원리는 동일하나, 미소유 옵셔널 참조는 nil
이 될 수 있다는게 차이점이다.
옵셔널이 아닌 미소유 참조처럼, nextCourse
가 항상 메모리 해제 되지 않은 코스를 참조하도록 보장해야한다. 이 경우, 예를 들어,department.courses
에서 코스를 제거하는 경우, 지운 과정을 참조하는 다른 과정 역시 지워야 한다.
Unowned References and Implicitly Unwrapped Optional Properties
지금까지 봤던 약한 참조, 미소유 참조 예시를 정리해보자.
Person
과 Apartment
예시는 두 개의 프로퍼티가 모두 nil
이 될 수 있었고, 이는 강한 순환 참조를 야기할 가능성이 있었다. 이는 weak
참조로 해결되었다.
Customer
와 CreditCard
예시는 하나의 프로퍼티가 nil
이 될 수 있었고 다른 프로퍼티는 nil
이 될 수 없는 상황에서 강한 순환 참조를 야기할 가능성이 있었다. 이 경우는 unowned
참조로 해결되었다.
하지만, 세 번째 경우에는 두 개의 프로퍼티들은 모두 값을 가지고 있어야 하고, 초기화 이후에도 nil
이 되면 안된다. 이 때는 암시적 언래핑과 미소유 참조를 함께 사용하면 해결할 수 있다.
이는 초기화 이후에 프로퍼티들에 옵셔널 언래핑 없이 바로 접근 가능하도록 해주고, 강한 순환 참조 또한 피하게 해준다.
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
의 이니셜라이저에서도 호출된다. 하지만 Country
의 이니셜라이저는 Country
객체가 완전히 초기화 될 때 까지,City
의 이니셜라이저에 self
를 넘길 수 없다.
그래서 Country
의 capitalCity
를 암시적 언래핑한 프로퍼티로 성정하여, nil
을 기본 값으로 갖도록 한다. 옵셔널과 동일해보이지만, 따로 언래핑을 해줄 필요가 없다. (암시적으로 언래핑한 옵셔널은 값에 접근할 때마다 옵셔널 값을 강제 언래핑 하는 것과 같음)
capitalCity
가 nil
값을 초깃값으로 가지고 있기에, name
만 이니셜라이저에서 초기화하면, Country
객체는 완전히 초기화되었다고 볼 수 있다. 이는 곧 Country
이니셜라이저가 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
지금까지 두 개의 클래스 인스턴스 프로퍼티가 서로 강한 참조를 가져서 강한 순환 참조가 생기는 경우를 봤다. 또한 강한 순환 참조를 끊기 위해 weak
과 unowned
를 어떻게 사용하는지도 봤다.
강한 순환 참조는 클래스 객체의 프로퍼티에 클로저를 할당하고, 바디부분에서 객체를 캡쳐(capture)하면 강한 순환 참조가 발생한다. 클로저의 바디에서 객체의 프로퍼티에 접근하면서 발생하게 되는 것이다.
self.someProperty
나 self.someMethod
처럼 객체의 프로퍼티나 메서드에 접근하는 경우 클로저 자체가 "캡쳐"되어 강한 순환 참조를 발생시킨다.
강한 순환 참조는 클로저나 클래스 같은 참조 타입에서 발생한다. 클로저를 프로퍼티에 할당할 때, 클로저에 참조를 할당하는 것과 같다. 본질적으로 위와 같은 문제를 발생시킨다. 이번에는 두 클래스 객체 간의 문제가 아니라 클래스 객체와 클로져 사이에서 발생하는 문제에 대해서 알아볼 것이다.
Swift는 이를 해결하기 위해 클로저 캡쳐 리스트(closure capture list)라는 것을 제공한다. 하지만 이를 사용하기 전에 어떻게 순환 참조가 발생하는지 먼저 알아보자.
아래 예시는 클로저가 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
클래스는 요소의 이름을 나타내는 name
프로퍼티를 정의한다. HTML에서 "h", "p" 같은 것들이다. 또한 요소에서 텍스트를 나타내는 text
는 옵셔널로 정의한다.
또 asHTML
이라는 지연 프로퍼티를 정의한다. 이 프로퍼티는 name
과 text
를 조합하여 HTML 문자를 만드는 클로저를 참조로 한다. 이는 () -> String
타입으로 함수는 파라미터가 없고 문자열 값을 리턴한다는 것을 의미한다.
아래는 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
객체와 클로저 간에 강한 순환 참조가 발생하게 된다.
객체의 asHTML
프로퍼티는 클로저에 강한 참조를 가진다. 클로저가 바디에서 self
를 참조하면서, 클로저는 스스로를 캡쳐해버리게 되어 강한 순환 참조가 발생하게 된다.
paragraph
에 nil
을 할당하여 HTMLElement
인스턴스에 대한 강한 참조를 끊어도, 클로저와 HTMLElement
는 메모리 해제되지 않는다.
paragraph = nil
Resolving Strong Reference Cycles for Closures
클로저를 정의할 때 캡처 리스트(capture list)를 정의하여 객체와 클로저 간의 강한 순환 참조를 해결할 수 있다. 캡처 리스트는 클로저의 바디에서 하나 혹은 그 이상의 참조 타입을 캡쳐할 때 사용할 규칙을 정의한다. 마찬가지로 강한 참조 대신에 weak
, unowned
를 상황에 맞게 사용하여 강한 순환 참조를 피할 수 있다.
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
클로저와 캡처하는 객체가 항상 서로를 참조하고, 동시에 메모리에서 해제되는 경우 unowned
키워드를 사용하여 미소유 참조를 사용한다.
반대로, 캡쳐된 참조가 nil
이 될 수 있다면 약한 참조를 사용한다. 약한 참조는 항상 옵셔널 타입이기 때문에 메모리에서 참조가 해제되면 자동적으로 nil
이 된다.
위 HTML 예시는 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]
이며, 이는 강한 참조가 아닌 미소유 참조로 self를 캡쳐한다는 것을 의미한다.
나머지는 동일하게 생성하고 출력해볼 수 있다.
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
이제 참조는 다음과 같이 보이게 된다.
이제 클로저의 self
가 unowned
이기 때문에 HTMLElement
와 강한 참조를 유지하지 않는다. 만약 paragraph
변수를 nil
로 하여 강한 참조를 끊는다면 HTMLElement
는 메모리 해제되며, 디이니셜라이저 또한 호출된다.
paragraph = nil
// Prints "p is being deinitialized"
가비지 컬렉션과 ARC 비교 부분은 다른 글에서 다뤄보자!
Ref:
https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html