개발자의 삽질
[Swift] ARC 에 대해서 알아보자 본문
https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html
오늘은 스위프트의 ARC (Automatic Reference Counting)을 알아보자.
가장 좋은 건 역시 공식 문서이나, 영문을 최대한 번역 및 요약 정리 해보았다! 보고 싶은 사람은 위의 페이지로!
스위프트는 Automatic Reference Counting 을 이용해서 앱의 메모리를 관리한다.
대부분의 경우에는 아주 잘 작동한다! 그래서 대부분의 경우 사용자가 메모리 관리에 대해 신경 쓰지 않아도 된다.
즉, ARC는 클래스 인스턴스가 더 이상 사용되지 않을 때 자동으로 메모리를 해제한다.
그러면 어떻게 작동할까?
사용자가 인스턴스를 생성하면 ARC는 인스턴스를 위해 메모리를 할당한다. 이 메모리는 인스턴스에 관한 정보를 갖고 있다. 또한 이 인스턴스가 더 이상 필요하지 않다면 메모리를 해제한다. 해제된 메모리는 당연히 다른 곳에 사용될 수 있다.
그러나, 아직 사용하고 있는 인스턴스의 메모리를 ARC가 해제해버린다면, 사용자는 더 이상 그 인스턴스의 프로퍼티나 메서드에 접근할 수 없을 것이고, 접근하려고 한다면 앱이 멈출 것이다.
ARC는 이러한 일을 방지하기 위해서 현재 프로퍼티, 상수, 변수들이 클래스 인스턴스를 참조하고 있는지 계속해서 추적한다. 여전히 그 클래스 인스턴스를 참조하고 있는 것이 있다면 ARC는 메모리 해제를 하지 않는다.
만약 프로퍼티, 상수, 변수들에 클래스 인스턴스가 할당되어 있다면 이러한 프로퍼티, 상수, 변수들은 strong reference 관계를 그 인스턴스와 갖는다. 그리고 이러한 관계가 남아있다면 ARC는 메모리 해제를 하지 않는다.
ARC in Action
예시를 통해 살펴보자!
class Person {
let name: String
init(name: String) {
self.name = name
// init 시에 출력
print("\(name) is being initialized")
}
deinit {
// deinit 시에 출력
print("\(name) is being deinitialized")
}
}
var reference1: Person?
var reference2: Person?
var reference3: Person?
// Optional 은 nil 로 초기화된다. 따라서 현재 Person 인스턴스를 참조하고 있지 않는다.
reference1 = Person(name: "John Appleseed")
// 출력결과 -> "Peter is being initialized"
이제 새 Person 인스턴스는 reference1 와 strong reference 관계를 갖는다.
이때, ARC는 이 Person 인스턴스를 메모리에 유지하고 해제하지 않는다.
reference2 = reference1
reference3 = reference1
이제 Person 인스턴스는 3개의 strong reference 관계를 갖는다.
reference1 = nil
reference2 = nil
만약 2개의 strong reference 관계를 끊어도 여전히 하나의 strong reference 관계가 남아있기 때문에 Person 인스턴스는 메모리에서 해제되지 않는다.
reference3 = nil
// 출력결과 -> "Peter is being deinitialized"
그러나 마지막으로 남은 strong reference 를 끊는다면 ARC는 Person 인스턴스를 메모리에서 해제된다.
이때, deinit에 설정해둔 출력을 하게 된다.
Strong Reference Cycles Between Class Instances
위의 예시에서 볼 수 있듯이, ARC는 계속해서 reference의 수를 추적할 수 있다.
그러나, 한 인스턴스 클래스(위의 예시에서 Person class)의 strong reference 갯수를 절대 0 개로 만들 수 없게 할 수 있다.
이는 2개의 클래스 인스턴스가 서로에 대해 strong reference 관계를 갖고 있다면 가능하다.
이를 strong reference cycle 이라고 부른다.
이를 해결하기 위해서는 strong reference 관계가 아니라 weak 또는 unowned refences 관계로 만든다면 해결할 수 있다.
그전에, 먼저 strong reference cycle에 대해 알아보자
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") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "peter")
unit4A = Apartment(unit: "4A")
위와 같이 코드를 치면 아래와 같은 strong reference 관계를 보여준다.
john!.apartment = unit4A
unit4A!.tenant = john
느낌표(!)가 등장했는데, 이는 john과
unit 4 A
옵셔널 변수 내에 저장된 인스턴스를 언래핑 하고 접근하기 위해 사용되므로 해당 인스턴스의 프로퍼티를 설정한다.
또한 이제 아래의 그림과 같이 각 인스턴스 간에 서로 strong reference를 가지게 되었다.
john = nil
unit4A = nil
이제 이렇게 john과 unit 4 A에 nil을 할당하더라도 deinitializer는 호출되지 않는다.
왜냐하면 여전히 strong reference 가 남아있기 때문이다. 또한 이는 메모리 누수 문제로 이어진다.
Resolving Strong Reference Cycles Between Class Instances
2가지의 해결책
- weak references
- unowned references
weak, unowned references는 하나의 인스턴스를 다른 인스턴스와 strong reference 관계없이 참조할 수 있게 해 준다.
Weak Reference를 사용해야 할 때
다른 인스턴스가 더 짧은 lifetime을 가지고 있을 때 사용하면 된다.
즉, 다른 인스턴스가 먼저 메모리에서 해제될 수 있는 경우를 말한다.
위의 Apartment에서는 tenant는 어느 순간 apartment의 lifecycle 에서 없을 수 있다. 따라서 이 경우에는 weak reference를 사용함으로써 reference cycle을 끊는 것이 적절하다.
Unowned Reference를 사용해야 할 때
다른 인스턴스가 같은 또는 더 긴 lifetime을 가지고 있을 때 사용하면 된다.
Weak References
weak reference는 다른 인스턴스를 강하게 잡는 reference 가 아니다, 따라서 ARC는 참조한 인스턴스를 계속해서 처리한다.
이러한 작용은 strong reference cycle을 방지한다.
weak reference 가 다른 인스턴스를 강하게 잡지 않기 때문에 weak reference 가 참조하고 있는 인스턴스가 메모리에서 해제되면 ARC는 자동으로 nil을 weak reference에 부여한다.
또한, weak reference는 이렇게 nil 값으로 설정될 수 있기 때문에 언제나 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") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "Peter")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
위의 코드는 이전과 거의 같지만 Apartment 클래스의 tenant 변수가 weak로 선언되어 있다.
john = nil
// Prints "John Appleseed is being deinitialized"
이렇게 john에 nil 값을 설정하면, Person 인스턴스에는 strong reference 가 더 이상 존재하지 않는다.
또한 tenant를 nil로 설정하게 된다.
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
이렇게 unit 4 A까지 nil 값을 설정하면 더 이상 Apartment 인스턴스에 strong reference 가 없다.
따라서 메모리에서 해제된다.
Unowned References
weak reference와 마찬가지로 unowned reference 도 다른 인스턴스를 강하게 잡지 않는다.
그러나 weak reference와 다르게 unowned reference는 다른 인스턴스가 같은 또는 더 긴 lifetime 을 가질 때 사용한다.
또한 weak reference 와 다르게 unowned reference 는 언제나 값을 가질 것을 요구합니다.
따라서 ARC는 절대로 unowned reference로 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") }
}
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
Customer 클래스에는 CreditCard 가 Optional 타입으로 선언되어 있다.
이는 갖고 있을 수도, 갖고 있지 않을 수도 있기 때문이다.
그러나 CreditCard 클래스는 Customer 가 unowned으로 선언되어 있다.
이는 신용카드는 반드시 Customer 가 존재하기 때문이다.
위의 코드를 따르면 아래와 같은 관계를 형성한다.
Customer 인스턴스는 CreditCard 인스턴스와 strong reference 관계이고 CreditCard 인스턴스는 Customer 인스턴스와 unowned reference 관계이다.
john 변수에 있던 strong reference를 끊는다면 Customer 인스턴스에는 더 이상 strong reference 가 없다.
따라서 메모리에서 해제된다.
이는 CreditCard 인스턴스에게 더 이상 strong reference 가 없기 때문에 함께 메모리에서 해제된다.
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
따라서 위와 같은 결과를 보인다.
Unowned Optional References
unowned 에도 optional reference 가 가능하다. unowned optional reference와 weak reference는 ARC 소유 모델 관점에서 같은 맥락에서 사용이 가능하다.
다만 다른 점은, 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에 대해 strong reference를 갖는다.
Course 클래스는 2 개의 unowned reference 를 갖는데 Department와 학생들이 다음으로 들어야 할 Course이다.
Course의 unowned reference 중 Department는 언제나 갖게 되지만 next Course는 가지지 않을 수도 있다. 따라서 이를 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]
위의 코드는 아래와 같은 관계를 가진다.
unowned optional reference는 인스턴스를 강하게 잡지 않기 때문에, 인스턴스를 메모리에서 해제하려고 하는 ARC의 작용에 대해 막지 않는다.
nil 값을 가질 수 있는 것을 제외하면 ARC 가 unowned reference에서 작동하는 것과 동일하게 작동한다.
non-optional unowned references와 같이 개발자는 nextCourse 가 언제나 메모리로부터 해제되지 않은 course를 참조하게끔 유의해야 한다.
만약 개발자가 department.courses 에서 course 를 제거했다면 그 course 를 참조하고 있던 모든 reference를 끊어야 한다.
Unowned References and Implicitly Unwrapped Optional Properties
Person, Apartment는 두 개가 다 nil 값을 가질 수 있기 때문에 strong reference cycle 문제가 나타날 수 있고 이는 weak reference로 해결할 수 있다.
Customer, CreditCard는 하나는 nil 값을 가질 수 있지만 다른 하나는 nil 값을 가질 수 없을 때 strong reference cycle 문제가 나타날 수 있고 이는 unowned reference로 해결할 수 있다.
이제 두 개의 프로퍼티가 언제나 값을 가져야 하고 결코 nil 값으로 설정되지 않아야 하는 경우는 어떻게 해야 할까?
이 경우 한 클래스 내에 있는 unowned property를 다른 클래스 내의 implicitly unwrapped optional property와 결합하면 된다.
이렇게 한다면 reference cycle을 피하면서도 두 개의 프로퍼티를 직접 접근할 수 있다.
아래의 예시를 보자
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 생성자는 새로운 Country 인스턴스가 완전히 생성되기 전까지 City 생성자에게 self를 줄 수 없다. 이를 2 단계 초기화라고 부른다. (Two-Phase Initialization)
이를 대처하기 위해 capitalCity 프로퍼티를 implicitly unwrapped optional property로 선언한다.
이는 capitalCity 프로퍼티가 optional 타입처럼 nil 값을 기본 값으로 갖게 한다. 그러나 unwrap 없이 여전히 접근할 수 있다.
이를 Implicitly Unwrapped Optionals라고 한다.
capitalCity의 기본 값이 nil 이므로 새로운 Country 인스턴스는 생성자를 통해 자신의 name 프로퍼티만 값을 주면 완전히 생성되게 된다.
다시 말하자면, Country 생성자는 name 프로퍼티에 값을 주면 implicit self 프로퍼티를 참조하고 전달할 수 있다.
이러한 방법은 strong reference 없이 Country와 City 인스턴스를 하나의 statement를 통해 생성할 수 있음을 보인다.
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"
Strong Reference Cycles for Closures
지금까지 strong reference cycles를 끊기 위해 여러 가지 방법들을 봤다. 아직 한 발 더 남았다.
strong reference cycle 은 클로져를 통해서도 만들어진다.
만약 어느 클래스의 프로퍼티에 인스턴스를 붙잡는 클로져를 할당할 때 strong reference cycle 이 만들어진다.
이러한 일이 발생하는 이유는 클로져도 참조 타입이기 때문이다.
프로퍼티에 클로져를 할당하면 그 클로져를 참조하게 된다. 이는 위에 있는 경우와 같은데 2개의 strong references 가 서로 계속 살아있게 만든다.
swift는 이 경우 closure capture list를 이용해 문제를 해결한다.
해결법을 알기 전에 먼저 예시를 통해 strong reference cycle 이 만들어지는 것부터 확인하자
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를 살펴보면 asHTML이라는 lazy property 가 있다.
이 프로퍼티는 내부에 name과 text를 포함하는 클로져를 참조한다.
이 asHTML 은 인스턴스 메서드처럼 생겼지만 그냥 클로져 프로퍼티이다. 따라서 default 값을 네가 원하는 클로져로 바꿀 수 있다.
아래를 보자.
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>"
이와 같이 선언한 paragraph 변수는 아래와 같이 strong reference cycle을 만들게 된다.
HTMLElement 인스턴스는 asHTML 프로퍼를 strong reference로 붙잡는다.
그러나 클로져 내부에 self는 클로져가 다시 HTMLElement 인스턴스를 strong reference 하게 한다.
이렇게 둘 사이에 strong reference cycle 이 만들어진다.
paragraph = nil
// 아무 것도 출력되지 않는다. 메모리가 해제되지 않았기 때문에
Resolving Strong Reference Cycles for Closures
Capture list를 이용해서 strong reference cycle을 해결할 수 있다.
Capture list는 클로져 안에서 한개 또는 그 이상의 참조 타입을 가질 때 규칙을 선언한다.
Defining a Capture List
capture list 안에는 weak, unowned 키워드를 가진 클래스 인스턴스(self)나 값이 있는 변수가 있다 (delegate = self.delegate)
capture list 는 변수나 반환 타입 이전에 둔다.
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
closure 안에서 unowned reference는 클로져가 capture 한 인스턴스 간 항상 참조할 것이고 언제나 함께 메모리에서 해제된다.
반대로, weak reference로 선언하면 나중에 nil 값이 될 수도 있다.
위에 있는 HTMLElement 클래스에 unowned reference를 이용해 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")
}
}
위에서 선언한 HTMLElement와 유일하게 다른 건 asHTML에 [unowned self]라는 capture list 가 있는 것이다.
이를 통해 아래 그림과 같은 관계를 가질 수 있게 한다.
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
paragraph = nil
// Prints "p is being deinitialized"
unowned reference로 인해 paragraph의 strong reference를 끊는다면 HTMLElement 인스턴스의 메모리는 해제된다.
따라서 프린트 값을 볼 수 있다.
이상으로 ARC에 대한 글을 마무리한다.
상당히 긴 글이지만, 꼭 알아두면 좋은 개념이다.
'Swift' 카테고리의 다른 글
[Swift] Structure vs Class 무엇을 골라야 할까? (0) | 2022.01.14 |
---|---|
[Swift] Queues & Threads - Concurrency by Tutorials 2편 (0) | 2022.01.13 |
[Swift] Inheritance (1) | 2022.01.12 |
[Swift] Enumerations 에 대해 알아보자 (0) | 2022.01.01 |
[Swift] GCD & Operations - Concurrency by Tutorials 1편 (0) | 2021.12.20 |