지난번에 ARC가 어떤 식으로 메모리를 관리하는지, 강한 참조 순환 문제는 어떻게 예방할 수 있는지에 대해 알아보았고, 이번에는 그 외의 공식문서상에 나와있는 내용들에 대해 공부해 보고자 한다.
✏️ Unowned Optional References(미소유 옵셔널 참조)
이전에 공부한 내용으로는 미소유참조의 경우 옵셔널이 아닌 인스턴스를 참조할 수 있다고 했다. 그럼 옵셔널인 인스턴스를 참조하면 안 되는 것인가?라고 물으면 그 답은 No, 할 수 있다! 이다.
Unowned의 옵셔널 참조는 사실 weak reference와 같은 상황에 할 수 있다고 한다.(ARC 소유 모델에 따르면) 그러나, 미소유 참조의 경우 인스턴스가 사라지더라도 nil을 자동으로 할당해 주지 않는다. 그렇기 때문에 미소유 옵셔널 참조를 하기 위해서는 유효한 객체를 가리키거나 그렇지 않을 경우 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
}
}
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 var department: Department 이 부분을 통해서 Department와 Course의 인스턴스 간 강한 참조 순환 문제를 예방하고 있음을 알 수 있으며, 동시에 unowned var nextCourse: Course? 이 부분을 통해(미소유 옵셔널 참조) 또 한 번 방지하고 있다.
여기서 중요한 것은, unowned optional reference를 사용한(코드를 읽어보면, nextCourse라는 의미 때문에 optional을 사용한 것을 알 수 있다.) 부분은 nil을 할당해 줄 수 있지만 문제는 ARC가 직접 해주지 않는다는 것이다.(weak reference였다면, 자동으로 해줬을 것이다.)
그러니, 이렇게 미소유 옵셔널 참조를 사용한 경우에는 그 인스턴스가 메모리에서 사라질 때 꼭 nil로 직접 할당해주지 않으면 runtime error가 발생하는 것을 알아야 할 것이다!
✏️ Unowned Reference & Implicitly Unwrapped Optional Properties(미소유 참조와 암시적 추출 옵셔널 프로퍼티)
약한 참조와 미소유 참조를 통해서 강한 참조 순환 문제를 해결할 수 있었다. 그런데 또 다른 상황이 있다.
서로 참조해야 하는 프로퍼티에 값이 꼭 있어야 하는데, 동시에 한 번 초기화되면 그 이후에는 nil을 할당할 수 없는 조건이다. 아래 예시는 Country클래스와 City클래스를 통해, 국가와 수도의 관계를 보여준다.
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
}
}
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\\(country.name)'s capital city is called \\(country.capitalCity.name)")
// "Canada's capital city is called Ottawa"
한 국가는 반드시 수도를 가지고, 어떤 도시는 반드시 어떤 국가에 귀속된다.
여기서 Country타입의 인스턴스를 생성할 때 capitalCity 프로퍼티를 초기화하는 과정에서 자기 자신 자신(Country)을 참조하도록 보내줘야 한다. 이럴 경우 capitalCity프로퍼티를 암시적 추출 옵셔널로 선언해줘야만 한다! (그렇지 않으면, Country인스턴스가 생성되기 전까지 country: self 같은 부분을 사용할 수 없기 때문)
또한, City 클래스의 country프로퍼티가 Country타입의 인스턴스를 참조할 것인데, 이때 Country는 옵셔널이 될 수 없다.(모든 도시는 국가에 귀속되므로)
→ 옵셔널이 아니므로 weak reference사용 불가 → unowned reference 상수로 참조
*당연히 이 모든 것은 Country타입의 인스턴스와 City타입의 인스턴스가 서로 강한 참조 순환 문제를 유발할 수 있기에 이를 방지하기 위한 목적으로부터 시작된 방법이다.
총 정리해보면, 암시적 추출 옵셔널 프로퍼티는 이니셜 라이저의 2단계 초기화 조건을 충족시키기 위해 사용했으며 미소유 참조 프로퍼티는 약한 참조를 사용할 수 없는 경우(옵셔널이 아니고 상수 사용)에 강한 참조를 피하기 위해 사용되었다!😎
✏️ Strong Reference Cycles for Closures
지금까지는 인스턴스들끼리의 강한 참조 순환 문제와 그 해결에 대해 공부했는데, 클로저가 인스턴스의 프로퍼티일 때와 클로저의 Capturing value 특성 때문에도 나타날 수 있다.
여기서 capturing value특성에 대한 것은 예를 들어서 클로저 내부에서 self.someProperty로 인스턴스의 프로퍼티에 접근하거나 또는 클로저 내부에서 self.someMethod()와 같이 인스턴스의 메서드를 호출할 때 클로저가 self를 획득(capture) 하기 때문에 강한 참조 순환이 발생한다.
클로저의 강한참조 순환 문제는 획득 목록(Capture list)을 통해서 해결한다고 한다. 이는 뒤에서 봐야 할 것 같고 우선 클로저의 강한 참조 순환이 나타나는 예시를 통해 어떻게 문제가 발생하는지 확인해보자.
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")
}
}
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// "<p>hello, world</p>"
paragraph = nil
// deinitializer 호출되지 않음!
위의 코드에서 lazy키워드를 통해 asHTML 프로퍼티를 지연 저장 프로퍼티로 만든 이유는, 클로저 내부에서 self를 통해 인스턴스 프로퍼티에 접근하기 때문이다.
여기서 asHTML프로퍼티는 () → String 타입의 클로저이다. 클로저를 호출하면 그 내부에 있는 참조 타입 변수 등을 획득함으로서 강한 참조 횟수를 증가시킨다. 그리하여 메모리에서 그 참조들을 사용할 수 있도록 보호하는데 이것이 문제가 되는 경우가 있다. 클로저 내부에서 자신을 프로퍼티로 갖는 인스턴스가 있다면, 그 인스턴스의 강한참조 횟수도 증가시키기 때문이다.
그렇기 때문에 위의 asHTML프로퍼티로 클로저를 호출하게 되면, 내부에 있는 self라는 참조타입 인스턴스의 강한참조 카운트를 +1 시키게 되는 것이다.
이 그림과 같이, HTMEElement의 인스턴스가 asHTML프로퍼티를 통해 () → String 타입 클로저를 참조하여 강한 참조함과 동시에, 클로저는 호출되면 그 내부에서 self를 통해 HTMLElement 인스턴스의 강한참조 카운트를 증가시켜버리는 것이다.(capture)
→ 강한참조 순환 발생 → paragraph = nil을 할당해도 HTMLElement의 인스턴스는 메모리에서 해제되지 않음!
단, self를 여러 번 호출하여도 참조 횟수는 1번만 증가!
✏️ Capture list(획득 목록)
획득 목록이란 클로저 내부에서 참조 타입을 획득할 때 어떤 식으로 획득할지 규칙을 지정해 주는 기능이다. 그러니까 위의 클로저의 강한 참조 순환 예시에서 asHTML프로퍼티로 할당된 () → String 타입의 클로저는 self를 강한 참조 하는데, 이때 어떤 참조(강한, 약한, 미소유)를 할지 규칙을 정해주는 기능!
획득 목록은 참조 방식과 참조할 대상을 대괄호([])로 감싸서 목록 형식으로 작성하며, 이 뒤에는 in키워드를 써줘야 한다. 그런데, 획득 목록에 명시한 요소가 값 타입이냐 참조 타입이냐에 따라서 클로저가 다르게 동작하니 예시를 통해 확인해보자.
var a = 0
var b = 0
let closure = { [a] in
print(a, b)
b = 20
}
a = 10
b = 10
closure() // 0 10
print(b) // 20
이 경우 a, b는 Int타입의 인스턴스로서 값 타입이다. closure에는 a만을 획득 목록으로 하지정였고, b는 지정하지 않았다.
예시를 통해 알 수 있는 것은, 값 타입을 획득목록으로 지정해준다면, 클로저 생성 시점에 값 타입 요소는 초기화된다는 것이다. 그렇기 때문에 a는 클로저 내의 a라는 완전히 새로운 요소로 다시 만들어져서, 실제 위 코드에서 a는 클로저 외부, 내부의 것이 각각 존재하고 b는 하나가 존재하고 있는 것이다.
반면 참조 타입의 획득목록 동작은 조금 상이하다.
class SimpleClass {
var value: Int = 0
}
var x = SimpleClass()
var y = SimpleClass()
let closure = { [x] in
print(x.value, y.value
}
x.value = 10
y.value = 10
closure() // 10 10
참조타입의 변수 x, y는 클로저 내에서 x만 획득 목록에 지정하고, y는 하지 않았음에도 두 요소의 결과가 같은 것을 알 수 있다. 어떻게 생각하면 값 타입과 참조 타입의 정의를 알고 있다면 당연한 현상인 것 같기도 하다.🤔
대신 획득 목록을 통해서 지정해줄 때 어떤 방식으로 참조할 것인지(강한, 약한, 미소유)를 지정해줄 수 있다.
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")
}
}
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// "<p>hello, world</p>"
paragraph = nil
// "p is being deinitialized"
아까의 코드에서 asHTML 프로퍼티 내부 클로저 정의에서 [unowned self]를 통해 획득 목록을 지정하고 in키워드를 생략하지 않고 써줬다. 이랬을 때 인스턴스의 참조 상황은 다음과 같다.
이제 () → String 타입의 클로저는 HTMLElement의 인스턴스를 미소유 참조하고 있음을 알 수 있고, 덕분에 paragraph가 nil로 할당될 때 HTMLElement의 강한 참조 카운트는 0이 되어 메모리에서 해제될 수 있게 된다. 그에 따라 deinitializer가 호출된 것을 확인할 수 있다.
🍎 결론
사실 ARC를 공부한 이유는 [unowned self]라고 적힌 코드를 보고 ‘아 이거 처음 보는 내용인데...’ 하고 찾다 보니 ARC라는 주제를 공부하면 알 수 있을 것 같아서였다. 이제 그 코드를 다시 보러 가야겠다.
일단, ARC라는 것이 어떤 역할을 하는지 알겠고, 대강 약한 참조와 미소유 참조를 통해 순환 문제를 해결할 수 있다는 것도 알겠으나. 클로저는 역시나 헷갈리는 요소이다.🤮
많이 사용하면서 익숙해지면 좋겠다.
🍎 참고자료
야곰님 Swift Programming 3판
'공부 > [iOS&Swift]' 카테고리의 다른 글
[iOS] Storyboard library component들에 대하여 (0) | 2022.02.10 |
---|---|
[iOS] Xcode - Info.plist에 대하여 (0) | 2022.02.10 |
[iOS] Xcode - Target, Project, Scheme, Build setting 에 대하여 (0) | 2022.02.07 |
[Swift] ARC(Automatic Reference Counting) - 1 (0) | 2022.02.05 |
[iOS] AVFoundation, AVAudioPlayer, Timer (0) | 2022.02.02 |
댓글